前言

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

话题的来源在最近需要一款高精度的编码器,于是选了海德汉的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位角度值。!
---------------------
作者:呐咯密密
链接:https://bbs.21ic.com/icview-3284258-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐