1、介绍
之前项目刚好有用到禹衡家的17位绝对值编码器,趁着周末有时间整理一下开发思路,同时也分享出来给有需要的人做做参考。
说回编码器,我们都知道在伺服控制中,为了获取更高的位置精度,完成更精细的绝对定位,通常会采用绝对式光电编码器作为位置反馈传感器,其常见的两种主要是17位与23位,编码器厂商为了增强信息传输可靠性,编码信息一般会采用串行方式输出,通过特定的通讯协议控制。这里我们主要介绍一下17位绝对式光电编码器。
2、编码器
2.1、接口定义
说明:实际应用过程中,如果没有多圈值记录需求,可以不接电池,但如果需要长时间保留多圈值数据,比如多轴机器人的原点数据,这种情况则需要接入电池。
2.2、电气参数
说明:1、输出信号符合RS485接口,负载电流Imax:±50mA;
2、消耗电流:≤120mA(无负载情况);
3、绝缘阻抗10MΩ以上(0V与壳之间,DC500V);
2.2、通讯电路
说明:1、IC1为线性差分驱动器,功能等同于MAX485;
2、IC2为线性差分接收器,功能等同于MAX485,推荐使用ADM485;
3、匹配电阻:编码器匹配电阻R2为220Ω,推荐用户接收端匹配电阻R5≥120Ω;
4、上/下拉电阻:编码器上/下拉电阻R1、R3为4.7KΩ,推荐用户接收端上/下拉电阻R4、R6不小于1KΩ。
2.3、协议
根据编码器电气参数与通讯电路,我们可以确定通讯方式,STM32采用USART模块,串口波特率设置2.5Mbps,以下是多摩川协议时序图:
说明:1、CF:控制域,用户发送命令CF与编码器返回的CF一致;
2、SF:状态域,编码器返回错误信息与报警信息;
3、DF:数据域,编码器返回数据;
4、CRC:校验域,CRC多项式:G(X)=𝑋8+1,校验范围CF、SF、DF。
2.3.1、控制字CF
说明:1、根据时序图可以知道发送请求只需要发送CF即可,具体CF值可以根据上表分析,CF结构主要是start bit(0)+sink code+Data ID code+ID parity+Delimiter(1),这里注意一下开始位(0)与结束位(1),与串口无校验发送里面的开始位与结束位一样的嘛,所以我们其实只需要发送sink code+Data ID code+ID parity就可以了;
2、根据时序图可以看到sink code 固定为010,剩下的Data ID code+ID parity看自己需求查表对应上就行,经常用的主要是ID0=0x02与ID3=0x1A,其余我就不说了,贴代码。
3、这里注意一下,编码器返回的报文CF位必须与发送CF一致,在处理接收报文时需校验一下。
/* 宏定义声明 ----------------------------------------------------------------*/ #define DataId0 0x02 /*读取单圈*/ #define DataId1 0x8A /*读取圈数*/ #define DataId2 0x92 /*读取编码器编号*/ #define DataId3 0x1A /*读取单圈+圈数*/ #define DataId6 0x32 /*写EEPROM*/ #define DataIdD 0xEA /*读EEPEOM*/ #define DataId7 0xBA /*重置错误ERROR*/ #define DataId8 0xC2 /*重置圈数*/ #define DataIdC 0x62 /*重置圈数与ERROR*/ |
2.3.2、状态字SF
说明:状态位SF主要反馈编码器状态,包括编码器是否过热、电池供电是否正常、数据解析是否正确等。
2.3.3、数据字DF
根据发出的CF不同,编码器返回的DF也会不同,具体的对应关系如下表:
说明:1、ABS0-ABS2:编码器单圈值,由于STM32的串口是小端模式,所以实际 编码器单圈位置值=(ABS2<<16)|(ABS1<<8)|(ABS0<<0);
2、ABM0-ABM2:编码器圈数值,实际编码器圈数值=(ABM2<<16)|(ABM1<<8)|(ABM0<<0)。
说明:ALMC:编码器错误,不同数值都有其对应的故障,对应查表即可。
2.4.4、校验字CRC
说明:这里注意一下,我们采用的是小端异或的计算方式,根据多项式 G(X)=𝑋8+1 其对应的二进制数为0x101,其Ploy值为0x01,LSB First=0x80,我们需要将CF+SF+DF以字节形式分别于LSB First进行亦或处理,最后结果为CRC8校验结果,具体实现过程见代码。
/****************************************************************************** * 函数功能: CRC8校验函数 * 输入参数: *message:接收的数据指针 len:数据长度 * 返 回 值: CRC8校验值 * 说 明: 多项式:G(X)=X^8+1 LSB first Poly: 0000 0001 LSB first : 1000 0000 =0X80 *****************************************************************************/ uint8_t APP_Math_CRC8_ChkValue(uint8_t *message, uint8_t len) { uint8_t crc_val; uint8_t i; crc_val = 0; while(len--) { crc_val ^= *message++; for(i = 0;i < 8;i++) { if(crc_val & 0x01) crc_val = (crc_val >> 1) ^ 0X80; else crc_val >>= 1; } } return crc_val; } |
3、单片机
3.1、单片机
当时项目为了上手快一点,又怕串口波特率不够,所以就选了个STM32F405RGT6,168MHz主频绰绰有余,现在想想有点杀鸡用牛刀了,这里主要提一下接下来要用到的串口与DMA。STM32F405有16数据流DMA,支持FIFOs模式与触发模式,4 个 USART/2 个 UART(速率可达10.5 Mbit/s,支持ISO7816接口)。
3.2、串口+DMA
串口(UART)是一种低速的串行异步通信,通常使用的波特率小于或等于115200bps,对于这种数据量不大的通信场景,一般没必要使用DMA,或者说使用DMA并未能充分发挥出DMA的作用。当面对数量大或者高波特率时,就必须使用DMA以释放CPU资源,因为高波特率可能带来CPU资源过度浪费的问题。这里编码器电气参数要求通讯速率≥2.5MHz,我们一般选择将串口波特率设置为2.5MHz,同时为了不占用太多CPU资源,CF发送与编码器数据接收都会采用DMA,具体寄存器设置如下:
/****************************************************************************** * 函数功能: USART2+DMA初始化函数 * 输入参数: 无 * 返 回 值: 无 * 说 明: 外部调用-多摩川编码器1 *****************************************************************************/ void BSP_Encoder1_InitConfig(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2); /*UART2-TX*/ GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2); /*UART2-RX*/ GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; /*UART2-TX*/ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_Init(GPIOA, &GPIO_InitStructure); /*UART2-RX*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; /*DIR1-PC15*/ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_ResetBits(GPIOC,GPIO_Pin_15); /*初始化关闭通信*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE); /*串口发DMA配置*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /*抢占优先级0-3,响应优先级0-3*/ NVIC_InitStructure.NVIC_IRQChannel=DMA1_Stream6_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; /*抢占优先级1*/ NVIC_InitStructure.NVIC_IRQChannelSubPriority=0; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&NVIC_InitStructure); /*DMA发送中断*/ DMA_DeInit(DMA1_Stream6); /*DMA通道配置*/ while(DMA_GetCmdStatus(DMA1_Stream6)!=DISABLE){} DMA_InitStructure.DMA_Channel = DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DR); /*外设地址*/ DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)Ed_Tx_Buf1; /*内存地址*/ DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; /*dma传输方向*/ DMA_InitStructure.DMA_BufferSize = USART_MAX_TX_LEN; /*设置DMA在传输时缓冲区的长度*/ DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; /*设置DMA的外设递增模式,一个外设*/ DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; /*设置DMA的内存递增模式*/ DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; /*外设数据字长*/ DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; /*内存数据字长*/ DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; /*设置DMA的传输模式*/ DMA_InitStructure.DMA_Priority = DMA_Priority_High; /*设置DMA的优先级别*/ DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; /*指定如果FIFO模式或直接模式将用于指定的流:不使能FIFO模式*/ DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; /*指定了FIFO阈值水平*/ DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; /*指定的Burst转移配置内存传输*/ DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; /*指定的Burst转移配置外围转移*/ DMA_Init(DMA1_Stream6, &DMA_InitStructure); /*配置DMA1的通道*/ DMA_ITConfig(DMA1_Stream6,DMA_IT_TC,ENABLE); /*使能中断*/
DMA_DeInit(DMA1_Stream5); /*串口接收DMA配置*/ while(DMA_GetCmdStatus(DMA1_Stream5)!=DISABLE){} DMA_InitStructure.DMA_Channel = DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DR); /*外设地址*/ DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)Ed_Rx_Buf1; /*内存地址*/ DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; /*DMA传输方向*/ DMA_InitStructure.DMA_BufferSize = USART_MAX_RX_LEN; /*设置DMA在传输时缓冲区的长度*/ DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; /*设置DMA的外设递增模式,一个外设*/ DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; /*设置DMA的内存递增模式*/ DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; /*外设数据字长*/ DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; /*内存数据字长*/ DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; /*设置DMA的传输模式*/ DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; /*设置DMA的优先级别*/ DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; /*指定如果FIFO模式或直接模式将用于指定的流 : 不使能FIFO模式*/ DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; /*指定了FIFO阈值水平*/ DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; /*指定的Burst转移配置内存传输*/ DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; /*指定的Burst转移配置外围转移*/ DMA_Init(DMA1_Stream5, &DMA_InitStructure); /*配置DMA1的通道*/ DMA_Cmd(DMA1_Stream5,ENABLE); /*使能通道*/ USART_InitStructure.USART_BaudRate = ENCODER1_BAUDRATE; /*配置8 1 0*/ USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No ; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; /*通道设置为串口中断*/ NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /*中断占先等级*/ NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; /*中断响应优先级*/ NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; /*打开中断*/ NVIC_Init(&NVIC_InitStructure); /*配置中断*/
USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE); /*采用DMA方式发送*/ USART_DMACmd(USART2,USART_DMAReq_Rx,ENABLE); /*采用DMA方式接收*/ USART_ITConfig(USART2,USART_IT_TC,DISABLE); /*中断配置*/ USART_ITConfig(USART2,USART_IT_RXNE,DISABLE); USART_ITConfig(USART2,USART_IT_TXE,DISABLE); USART_ITConfig(USART2,USART_IT_IDLE,ENABLE); /*开启USART2空中断*/ USART_Cmd(USART2, ENABLE); /*启动串口*/ } |
另附DMA1、DMA2每个控制的8个数据流和其对应的通道:
4、代码实现
至此串口DMA初始化完成,接下来就是具体的发送与接收,发送直接采用DMA传输完成中断,接收稍微需要点处理,因为我们接收的是不定长度的数据,所以需要通过串口的空闲中断来判断数据接收结束,数据接收完毕之后需要对CF、CRC进行校验,校验通过才可认为DF数据可用,具体代码实现如下:
/****************************************************************************** * 函数功能: USART2中断处理函数 * 输入参数: 无 * 返 回 值: 无 * 说 明: 外部调用-多摩川编码器1 *****************************************************************************/ void USART2_IRQHandler(void) /*串口2中断服务程序*/ { if(USART_GetITStatus(USART2,USART_IT_IDLE)!=RESET) /*空闲中断触发*/ { DMA_Cmd(DMA1_Stream5, DISABLE); /* 暂时关闭DMA数据尚未处理 */ ED_RX_LEN1 = USART_MAX_RX_LEN - DMA_GetCurrDataCounter(DMA1_Stream5); /* 获取接收到的数据长度*/ BSP_Encoder1_DMA_DataProcess(Ed_Tx_Buf1,Ed_Rx_Buf1,ED_RX_LEN1); /* 报文数据处理*/ DMA_ClearFlag(DMA1_Stream5,DMA_FLAG_TCIF5); /* 清DMA标志位 */ DMA_SetCurrDataCounter(DMA1_Stream5,USART_MAX_RX_LEN); /* 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目 */ DMA_Cmd(DMA1_Stream5, ENABLE); /*打开DMA*/ USART_ReceiveData(USART2); /*清除空闲中断标志位,接收函数有清标志位的作用*/ Ed_Flag_Rx_Busy1 = 1; /*状态位更新-USART2接收完成*/ } if(USART_GetFlagStatus(USART2,USART_IT_TXE)==RESET) /*串口发送完成*/ { Ed_Flag_Tx_Busy1=0; USART_ITConfig(USART2,USART_IT_TC,DISABLE); } } /****************************************************************************** * 函数功能: DMA1_Stream6中断处理函数 * 输入参数: 无 * 返 回 值: 无 * 说 明: 外部调用-多摩川编码器1 *****************************************************************************/ void DMA1_Stream6_IRQHandler(void) { if(DMA_GetFlagStatus(DMA1_Stream6,DMA_FLAG_TCIF6)!=RESET) /*等待DMA1_Steam6传输完成*/ { DMA_ClearFlag(DMA1_Stream6,DMA_FLAG_TCIF6); /*清除DMA1_Steam6传输完成标志*/ DMA_Cmd(DMA1_Stream6,DISABLE); /*关闭使能*/ USART_ITConfig(USART2,USART_IT_TC,ENABLE); /*打开串口发送完成中断*/ } } /****************************************************************************** * 函数功能: DMA_USART2发送函数 * 输入参数: *data:发送的数据指针 size:数据长度 * 返 回 值: 无 * 说 明: 外部调用-多摩川编码器1 *****************************************************************************/ void BSP_Encoder1_DMA_SendBuffer(u8 *data,u16 size) { Ed_Flag_Tx_Busy1=1; /*状态位更新-USART2发送中*/ memcpy(Ed_Tx_Buf1,data,size); /*复制数据到DMA发送缓存区*/ while (DMA_GetCmdStatus(DMA1_Stream6) != DISABLE); /*确保DMA可以被设置*/ DMA_SetCurrDataCounter(DMA1_Stream6,size); /*设置数据传输长度*/ DMA_Cmd(DMA1_Stream6,ENABLE); /*打开DMA数据流,开始发送*/ } /****************************************************************************** * 函数功能: DMA_USART2数据处理函数 * 输入参数: *data:接收的数据指针 size:数据长度 * 返 回 值: 无 * 说 明: 外部调用-多摩川编码器2 *****************************************************************************/ void BSP_Encoder1_DMA_DataProcess(u8 *txdata,u8 *rxdata,u16 size) { uint8_t crc=0; uint8_t buf[USART_MAX_RX_LEN]={0}; for(int i=0;i<size;i++) buf[i]=*rxdata++; if(buf[0]==*txdata) { crc=APP_Math_CRC8_ChkValue(buf,size-1); if(crc==buf[size-1]) { Encoder1_CodeValue=buf[4]<<16|buf[3]<<8|buf[2]; } else { Encoder_ReadFaultNum++; if(Encoder_ReadFaultNum>ED_READ_FAULT_MAXNUM) Encoder_ReadStatus=1; } } else { Encoder_ReadFaultNum++; if(Encoder_ReadFaultNum>ED_READ_FAULT_MAXNUM) Encoder_ReadStatus=1; } } |
5、结语文章来源:https://www.toymoban.com/news/detail-515256.html
整个项目其实难度并不大,对我个人而言花费时间多的主要还是在搞懂DMA、CRC校验原理、发送\接收中断处理这上面。本人对单片机也还是小白阶段,很多东西都是重头开始吸收学习,不过万变不离其宗,多花点时间搞清楚原理,很多不解的地方都会豁然开朗。技术分享就到这里了,希望能对你有所帮助!!文章来源地址https://www.toymoban.com/news/detail-515256.html
到了这里,关于基于STM32F4的多摩川协议通讯的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!