本帖最后由 呐咯密密 于 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差分电路 上述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位角度值。
|