发新帖本帖赏金 100.00元(功能说明)我要提问
返回列表
打印
[N32G430]

巧用N32G430外设,打破无EnDat协议的尴尬,读取海德汉编码器角度值

[复制链接]
9584|10
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 呐咯密密 于 2023-6-5 14:07 编辑

[url=home.php?mod=space&uid=760190]@21小跑堂 #申请原创#[/url]

次标题:单片机没有EnDat外设,如何读取海德汉编码器的绝对角度值?且看如何合理搭配普通外设冲破难关!
前言

好久没有写原创了,最近由于项目的紧张,也苦于无话题可写,恰逢项目空窗,对最近的开发过程做个记录。

话题的来源在最近需要一款高精度的编码器,于是选了海德汉的25位旋转编码器,分辨率高,精度高,价格也高,大几千块一台,而且交期很长很长,果然贵的东西除了贵,没啥缺点,当然,贵不是产品的缺点,是自己的缺点。而且在实际操作过程中发现EnDat2.2这种协议,在普通的单片机(此处选用国民技术的N32G430)上根本没有外设可以兼容,具体分析见下文。在搜寻全网,没有相关的经验借鉴,很多在使用该编码的大佬均采用FPGA来实现,想想也是,25位的角度数据,即使读取到完整的角度值,也不好用来开发更高级的应用,在单片机上采用普通的编码器就可以了。于是便有了下文的探索之路。

一、关于海德汉编码器

此处选用的是海德汉ECN425型号旋转编码器,分辨率为25bit,对应的每一转的位置数有33554432个,最大允许转速为12000rpm,精度为±20角秒。绝对值型号的可以直接通过EnDat 2.2协议直接输出25位绝对角度值,角度的计算时间最大只有5us。不仅位数高,精度还高,在精准控制方面属于首选编码器。


EnDat协议简介

因为数字驱动系统和反馈环在通过位置编码器获取位置值时,需要编码器快速传输数据和高可靠性传输数据,除了角度值以外,可能还需提供一些附加信息,比如驱动系统的相关参数,补偿参数等,同时还需要具有错误检测和诊断,于是海德汉公司为其编码器开发的一种双向数字接口,用于传输绝对式或增量编码器的位置值,于是便有了EnDat数据接口。EnDat 2.2可传输绝对式或增量式编码器的位置值,也能传输或更新保存在编码器中的信息或保存新信息。由于采用串行数据传输方式,它只需要四条信号线。数据传输保持与后续电子设备时钟信号同步。传输的数据类型(位置值、参数或诊断信息等)通过后续电子设备发至编码器的模式指令选择。纯串行的EnDat 2.2接口也适用于高安全性应用。编码器使用EnDat接口,在高分辨率的情况下同样可以支持短周期并提供换向信息,可以轻松满足直接驱动技术要求,整个读写的周期采样时间只有25us,对于后续的电子设备只需10us的时间就能得到位置值。

1.硬件接线

数据线只需要四根,两根差分的数据线和两根差分的时钟线,时钟最高可支持16MHz的传输速率。

2.协议详情

EnDat的协议在传输位置值的同时可以附带额外的数据包,此处应用无需此功能,仅获取25bit的绝对角度值。


EnDat协议数据包发送与数据传输同步,编码器会在时钟的第一个下降沿锁存当前角度值,两个时钟脉冲后控制器发送模式指令,如果只需要绝对角度值则指令模式为二进制000111,编码器在tcal时间内计算绝对位置值,后续开始向控制器连续发送数据。数据组成为:

1bit开始位+2bit错误位+25位角度数据+5bitCRC校验位

25bit绝对角度值传输为LSB,即最低有效位先传输,后传输高有效位,所以在控制获取数据后需要对绝对位置值进行转换。

二、N32G40单片机实现EnDat方案探究

EnDat数据的发送和接收共用一根数据线(不考虑差分),N32G430并没有EnDat的接口,想要实现绝对位置的读取就得利用现有的外设进行扩展,或者直接使用软件模拟,但是本人一向不喜欢软件模拟,抛开速度较低,且不能使用DMA就会占用CPU资源这些不谈,使用软件模拟就像下路选用炸*人打ADC,没有AD之魂。那么抛开软件模拟,我们就在现有的硬件外设上下功夫。

1.硬件SPI+RS485自动收发电路

该方案有两种组成电路,一种是使用自动收发的RS485芯片,一种是使用普通RS485芯片加上三极管实现自收发。

CLOCK差分电路
MAX13488自收发485芯片电路

上述CLOCK输入信号来源于SPI的CLK,经过SN75176实现时钟信号的差分,使用自动收发的485芯片-MAX13488,靠芯片内部电路实现自动切换使能信号,当SPI的MOSI信号到来切换为输出,将角度指令送出,发送完之后切换为输入,开始在MISO接收编码器的数据包。关于实现原理此处不赘述,可自行查询相关资料。

2.硬件SPI+普通485芯片+三极管电路
SN75176差分芯片+三极管切换电路

上述方案的CLOCK依旧采用上一个方案的电路,此处的RS485芯片的使能切换采用三极管控制,达到和上一方案相同的目的。在上述两种方案搭建完成后,发现切换使能的速度较慢,在高速通信时切换不过来,(具体情况未经测试,感兴趣自行研究)于是最终采用了以下方案:

3.SN75176+定时器捕获

上述方案SPI的MOSI和MISO依旧连接到差分芯片的发送和接收,使能引脚采用单片机的GPIO控制。该脚的高低电平由定时器控制,同时SPI的SCK连接到单片机的PA0,用于定时器2进行捕获。

根据EnDat协议,最多10个CLOCK后便可以等待编码器回传数据,使用定时器捕获到10个CLOCK脉冲后将使能拉低,SPI的MOSI发送的数据便被截断,可从MISO硬件获取编码器回传数据。

三、最终代码实现

大致流程如上图所示,SPI+DMA启动后根据协议拼装指令发送,定时器2设置为捕捉,同时开启中断。在SPI启动后CLOCK会产生时钟脉冲,并向编码器发送数据,此时485芯片使能保持为高电平,会将数据送入编码器,当送出10位数据后,定时器会捕获到10个脉冲产生中断,此时关闭定时器2,停止捕捉脉冲,并将485芯片的使能拉低,此时SPI的CLOCK不会停止,但是MOSI线上的数据被截断,但是编码器返回的数据会送至MISO线,直至整个数据包发送完成。而采用DMA就是维持整个时钟信号的稳定,不会因为定时器中断影响通信的完整性。

SPI初始化:SPI的初始化采用软件NSS,同时开启DMA使能,此处的NSS不用于片选,而用于控制RS485芯片的数据方向。

void SPI_Config(void)
{
        SPI_InitType SPI_InitStructure;        
        SPI_GPIO_Config();

        SPI_I2S_Reset(SPI_MASTER);

    SPI_Initializes_Structure(&SPI_InitStructure);
    SPI_InitStructure.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;
    SPI_InitStructure.SpiMode       = SPI_MODE_MASTER;
    SPI_InitStructure.DataLen       = SPI_DATA_SIZE_8BITS;
    SPI_InitStructure.CLKPOL        = SPI_CLKPOL_HIGH;
    SPI_InitStructure.CLKPHA        = SPI_CLKPHA_SECOND_EDGE;
    SPI_InitStructure.NSS           = SPI_NSS_SOFT;
    SPI_InitStructure.BaudRatePres  = SPI_BR_PRESCALER_128;
    SPI_InitStructure.FirstBit      = SPI_FB_MSB;
    SPI_InitStructure.CRCPoly       = 7;
    SPI_Initializes(SPI_MASTER, &SPI_InitStructure);
    SPI_Set_Nss_Level(SPI_MASTER, SPI_NSS_HIGH);

    SPI_I2S_DMA_Transfer_Enable(SPI_MASTER, SPI_I2S_DMA_TX);
    SPI_I2S_DMA_Transfer_Enable(SPI_MASTER, SPI_I2S_DMA_RX);
    SPI_ON(SPI_MASTER);
}

DMA初始化配置:
初始化同时配置DMA的CH1和CH2,用于SPI的收发,将SPI的收发均采用DMA来实现,防止中断的到来打断SPI的通信,
void SPI_DMA_Configuration(void)
{
        DMA_InitType DMA_InitStructure;
        DMA_Reset(DMA_CH1);
        DMA_Reset(DMA_CH2);

    /* SPI_MASTER TX DMA config */
    DMA_InitStructure.MemAddr = (uint32_t)&SPI_Master_Buffer_Tx[0];
    DMA_InitStructure.MemDataSize = DMA_MEM_DATA_WIDTH_BYTE;
    DMA_InitStructure.MemoryInc = DMA_MEM_INC_MODE_ENABLE;
    DMA_InitStructure.Direction = DMA_DIR_PERIPH_DST;
    DMA_InitStructure.PeriphAddr = (uint32_t)&SPI_MASTER->DAT;
    DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_WIDTH_BYTE;
    DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_MODE_DISABLE;
    DMA_InitStructure.BufSize = 6;
    DMA_InitStructure.CircularMode = DMA_CIRCULAR_MODE_DISABLE;
    DMA_InitStructure.Mem2Mem = DMA_MEM2MEM_DISABLE;
    DMA_InitStructure.Priority = DMA_CH_PRIORITY_HIGHEST;
    DMA_Initializes(DMA_CH1, &DMA_InitStructure);
    DMA_Channel_Request_Remap(DMA_CH1, SPI_MASTER_DMA_TX_CH);
   
    /* SPI_MASTER RX DMA config */
    DMA_InitStructure.MemAddr = (uint32_t)&SPI_Master_Buffer_Rx[0];
    DMA_InitStructure.Direction = DMA_DIR_PERIPH_SRC;
    DMA_InitStructure.Priority = DMA_CH_PRIORITY_HIGHEST;
    DMA_Initializes(DMA_CH2, &DMA_InitStructure);
    DMA_Channel_Request_Remap(DMA_CH2, SPI_MASTER_DMA_RX_CH);        
        
    DMA_Channel_Disable(DMA_CH1);
    DMA_Channel_Disable(DMA_CH2);
}


定时器配置:

这里使用TIMER2的CH1作为捕获通道,对应单片机的引脚是PA0,于是对TIMER2 CH1进行相关初始化:

首先初始化PA0,使用复用模式的AF3复用为TIMER2 CH1,配置TIMER2的中断,捕获到相应数量的方波后触发中断,由于国民技术的库对中断配置进行了封装,这里直接调用Common_TIM_NVIC_Initialize(TIM2_IRQn, ENABLE);便可配置完成中断,与函数下方被屏蔽部分效果相同,但是这里注意,如果有多个定时器中断,不可都调用此函数,否则中断会拥有相同的优先级,此处建议使用被屏蔽部分代码来配置,或者重写中断配置函数,使中断优先级变得可配置。

Common_TIM_Base_Initialize(TIM2,9,0);函数则是定义了定时器的预分频系数和重装载系数,9为重装载值,意为计数值为10个脉冲可触发溢出中断。

void Timer2_Config(void)
{
        
        NVIC_InitType NVIC_InitStructure;
    GPIO_InitType GPIO_InitStructure;
    GPIO_Structure_Initialize(&GPIO_InitStructure);
        
        GPIO_InitStructure.Pin            = GPIO_PIN_0;
        GPIO_InitStructure.GPIO_Mode      = GPIO_MODE_INPUT;
        GPIO_InitStructure.GPIO_Current   = GPIO_DS_12MA;
        GPIO_InitStructure.GPIO_Alternate = GPIO_AF3_TIM2;
        GPIO_Peripheral_Initialize(GPIOA, &GPIO_InitStructure);

    /* NVIC configuration */
    Common_TIM_NVIC_Initialize(TIM2_IRQn, ENABLE);        
   
//    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
//    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
//    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//          NVIC_Initializes(&NVIC_InitStructure);
        
        Common_TIM_Base_Initialize(TIM2,9,0);
/** Time Base Config */
        
    TIM_ICInitStructure.Channel     = TIM_CH_1;
    TIM_ICInitStructure.IcPolarity  = TIM_IC_POLARITY_RISING;
    TIM_ICInitStructure.IcSelection = TIM_IC_SELECTION_DIRECTTI;
    TIM_ICInitStructure.IcPrescaler = TIM_IC_PSC_DIV1;
    TIM_ICInitStructure.IcFilter    = 0x0;
    TIM_PWM_Input_Channel_Config(TIM2, &TIM_ICInitStructure);
   
    TIM_Trigger_Source_Select(TIM2, TIM_TRIG_SEL_TI1FP1);

    TIM_Slave_Mode_Select(TIM2, TIM_SLAVE_MODE_EXT1);

    TIM_Master_Slave_Mode_Set(TIM2, TIM_MASTER_SLAVE_MODE_ENABLE);
        TIM_Interrupt_Status_Clear(TIM2, TIM_INT_UPDATE);
    /* TIM enable counter */
    TIM_On(TIM2);

    /* Enable the CC1 and CC2 Interrupt Request */
    TIM_Interrupt_Enable(TIM2, TIM_INT_UPDATE);
        
}

SPI DMA启动函数:
void SPI_DMA_WriteReadByte(uint16_t BufferLength)
{
        NSS_High;  //拉高NSS引脚,实际控制为485芯片使能,方向为输出
        TIM_On(TIM2);//启动TIM2,开始捕捉脉冲

        DMA_Channel_Disable(DMA_CH1);
        DMA_Buffer_Size_Config(DMA_CH1,BufferLength);
        DMA_Channel_Enable(DMA_CH1);
        
        DMA_Channel_Disable(DMA_CH2);
        DMA_Buffer_Size_Config(DMA_CH2,BufferLength);
        DMA_Channel_Enable(DMA_CH2);        
        
    while(DMA_Flag_Status_Get(DMA, DMA_CH1_TXCF) == RESET);
    while(DMA_Flag_Status_Get(DMA, DMA_CH2_TXCF) == RESET);
}

启动DMA之前先将485的使能设为高电平,数据方向为发送,此时数据可从单片机发送到编码器,启动TIM2,开始捕捉脉冲,再启动DMA,控制SPI向编码器输出时钟和数据(如上代码),当SPI的时钟信号达到10和脉冲,触发TIM2的中断(如下代码)。

void TIM2_IRQHandler(void)
{
    if (TIM_Interrupt_Status_Get(TIM2, TIM_INT_UPDATE) != RESET)
    {
        TIM_Interrupt_Status_Clear(TIM2, TIM_INT_UPDATE);
                NSS_Low;
                TIM_Off(TIM2);
                TIM_Base_Count_Set(TIM2,0);
    }
}


在中断中关闭定时器捕获且将485芯片的使能拉低,此时SPI的CLK不会停止,但是MOSI数据被485芯片截断,MISO脚可以接收到编码器回传的数据。直到通信结束。之后再将收到的数据进行转换,获得此次通讯的角度值。

uint32_t Angle_Data_Processing(uint8_t *buffer)
{
        buffer[2] = BitReverseTable256[buffer[2]];
        buffer[3] = BitReverseTable256[buffer[3]];
        buffer[4] = BitReverseTable256[buffer[4]];
//        buffer[5] = buffer[5]&0xe0;
        buffer[5] = BitReverseTable256[buffer[5]];
        angle1 = buffer[5]<<24 | buffer[4]<<16 | buffer[3]<<8 | buffer[2] ;
        angle1 = (angle1>>1)&0x1FFFFFF;
        return angle1;
}
该代码将接收的角度数据进行大小端转换,此处采用查表法,将16位的小端数据转换成大端数据的查表储存,可直接进行转换。
static const unsigned char  BitReverseTable256[] =
{
        0X00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
        0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
        0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
        0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
        0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
        0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
        0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
        0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
        0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
        0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
        0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5,
        0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD,
        0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3,
        0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
        0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
        0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};


测试效果:

第一个为MISO的波形,白色为SPI的CLOCK波形,这三个8位再加上4C里面的最高位,就是25位角度值。



使用特权

评论回复

打赏榜单

21小跑堂 打赏了 100.00 元 2023-10-24
理由:恭喜通过原创审核!期待您更多的原创作品~

沙发
xch| | 2023-11-6 19:32 | 只看该作者


不是低位在前格式?



使用特权

评论回复
板凳
呐咯密密|  楼主 | 2023-11-7 09:19 | 只看该作者
xch 发表于 2023-11-6 19:32
不是低位在前格式?

是小端,获得的数据需要进行大小端转换

使用特权

评论回复
地板
xch| | 2023-11-7 10:50 | 只看该作者
呐咯密密 发表于 2023-11-7 09:19
是小端,获得的数据需要进行大小端转换

MCU 也是小端。没看懂为啥折腾

使用特权

评论回复
5
xch| | 2023-11-7 10:53 | 只看该作者
不是颠倒了?

使用特权

评论回复
6
xch| | 2023-11-7 10:57 | 只看该作者

使用特权

评论回复
7
xch| | 2023-11-7 10:58 | 只看该作者

使用特权

评论回复
8
xch| | 2023-11-7 10:59 | 只看该作者
二姨很奇葩。为了贯彻执行法西斯制度,贴图都禁了

使用特权

评论回复
9
xdfhx| | 2024-5-21 15:20 | 只看该作者
您好作者,我最近也在学习EnDat2.x协议,但是对于位置值的CRC算法有点疑惑,不知作者是否了解该CRC算法

使用特权

评论回复
10
szchen2017| | 2024-5-21 15:31 | 只看该作者
楼主厉害,学些了

使用特权

评论回复
11
zmyisky| | 2024-6-11 00:34 | 只看该作者
xdfhx 发表于 2024-5-21 15:20
您好作者,我最近也在学习EnDat2.x协议,但是对于位置值的CRC算法有点疑惑,不知作者是否了解该CRC算法 ...

请问你复现出来了吗

使用特权

评论回复
发新帖 本帖赏金 100.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:苏州澜宭自动化科技嵌入式工程师
简介:本人从事磁编码器研发工作,负责开发2500线增量式磁编码器以及17位、23位绝对值式磁编码器,拥有多年嵌入式开发经验,精通STM32、GD32、N32等多种品牌单片机,熟练使用单片机各种外设。

482

主题

3801

帖子

47

粉丝