最近项目上面的使用的温控表进行的产品的迭代升级,导致之前写的串口通讯部分代码出现了问题,这里和大家分享一些,在工作中的经验分享
一:硬件图纸如下所示:
这里设计的时候选用的SP3485通讯芯片;这里简单介绍一下SP3485芯片的主要特征如下所示:
兼容标准:符合RS-485和RS-422协议,支持半双工通信。
工作电压:+3.3V供电,兼容5V逻辑电平。
数据传输速率:最高可达10Mbps(取决于线路长度和条件)。
低功耗:
静态电流典型值300μA(待机模式更低)。
支持节电模式(通过使能引脚控制)。
驱动能力:最多支持32个节点并联(标准RS-485负载)。
抗干扰能力:
±15kV ESD保护(人体放电模型,HBM)。
总线引脚(A/B)具备±12V共模电压范围。
工作温度:工业级(-40°C至+85°C)。
当然这里选用的SP3485的主要原因还是和CPU的TTL信号的电平相同,毕竟选用的是3.3V的供电电压;
二:串口3的初始化部分:
这里串口3可以使用DMA的方式进行数据的传输,所以我当初配置的时候就选择使用DMA的方式,代码如下:
标准库:串口3的接收DMA初始化
void DMA_USART3_RX_Configuration(void) { DMA_InitTypeDef DMA_USART3_RX_InitStructure; //定义USART3的接收DMA结构体 DMA_DeInit(DMA1_Channel3); DMA_USART3_RX_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base; DMA_USART3_RX_InitStructure.DMA_MemoryBaseAddr = (u32)cUSART3RecvBuffer; DMA_USART3_RX_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_USART3_RX_InitStructure.DMA_BufferSize = USARTBUFRXLEN3; //接收产生中断的长度 DMA_USART3_RX_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_USART3_RX_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_USART3_RX_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_USART3_RX_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_USART3_RX_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式,接收数据循环存入 DMA_USART3_RX_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //最高优先级 DMA_USART3_RX_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_USART3_RX_InitStructure); }
串口3的发送DMA初始化部分:
void DMA_USART3_TX_Configuration(int length) { DMA_InitTypeDef DMA_USART3_TX_InitStructure; //定义USART3的发送DMA结构体,用于在发送的时候,重新初始化DMA通道 /* DMA1 Channel2 (triggered by USART3 Tx event) Config */ DMA_DeInit(DMA1_Channel2); DMA_USART3_TX_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base; DMA_USART3_TX_InitStructure.DMA_MemoryBaseAddr = (u32)cUSART3SendBuffer; DMA_USART3_TX_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_USART3_TX_InitStructure.DMA_BufferSize = length; //发送缓冲区USART3 DMA_USART3_TX_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_USART3_TX_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_USART3_TX_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_USART3_TX_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_USART3_TX_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_USART3_TX_InitStructure.DMA_Priority = DMA_Priority_High; //高优先级 DMA_USART3_TX_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel2, &DMA_USART3_TX_InitStructure); }
串口3的发送函数如下所示:
bool SendDataToUSART3(char length) { if(bUART3BusyFlag) return FALSE; GPIO_SetBits(GPIOB, GPIO_Pin_12);//RS485发送使能 bUART3BusyFlag = TRUE; iUSART3SendLength = length; iUSART3SendPoint = 0; USART_ITConfig(USART3,USART_IT_TXE, ENABLE); //使能串口中断 return TRUE; }
这里分享一下串口3的中断函数处理部分:
void USART3_IRQHandler(void) { u8 temp3; //RX if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) { temp3 = USART_ReceiveData(USART3); USART_ClearITPendingBit(USART3,USART_IT_RXNE); if(bUSART3_RecvOK==TRUE) //数据包未处理,不再接收 return; bUSART3OverTime = USART3OVT; //超时定时器 //读取接收的命令 cUSART3RecvBuffer[iUSART3RecvPoint] = temp3; iUSART3RecvPoint++; if(iUSART3RecvPoint >= 15) //已经接收到10个数据 { bUSART3_RecvOK = TRUE; iUSART3RecvPoint = 0; } } else //Tx 如果既非接受中断,也非发送中断,则判断是否为溢出中断 { //Tx if(USART_GetITStatus(USART3, USART_IT_TXE) != RESET) { temp3 = iUSART3SendLength; USART_SendData(USART3, cUSART3SendBuffer[iUSART3SendPoint++]); if(iUSART3SendPoint >= temp3) //发送完毕 { iUSART3SendPoint = 0; USART_ITConfig(USART3, USART_IT_TXE, DISABLE); bUART3BusyFlag = FALSE; //发送完毕,复位忙标志 GPIO_ResetBits(GPIOB, GPIO_Pin_12); //接受使能 } } else if(USART_GetFlagStatus(USART3, USART_FLAG_ORE) != RESET) temp3 = USART_ReceiveData(USART3); //依次读取USART_SR和USART_DR寄存器,可以清零ORE标志 } }
串口3的中断处理函数,当初是在串口1的通讯部分移植过来的,在和串口工业屏的通讯时候,没有发现问题,当时就觉得这样写也是没有问题的,当判断 发送完成中断时候,将SP3485的接收引脚切换到接收撞到就好。
然后分享一下读取温度表的当前温度的函数:
/*************************************************************************************** 通过Modbus协议读取温度值 ***************************************************************************************/ void FT807ModbusReadTemp(u8 Addr) { unsigned short ui16CRC; cUSART3SendBuffer[0] = Addr; cUSART3SendBuffer[1] = 0x04; cUSART3SendBuffer[2] = 0x00; cUSART3SendBuffer[3] = 0x00; cUSART3SendBuffer[4] = 0x00; cUSART3SendBuffer[5] = 0x01; ui16CRC = ClcCRC16(&cUSART3SendBuffer[0],6); cUSART3SendBuffer[6] = ui16CRC>>8; cUSART3SendBuffer[7] = ui16CRC; cUSART3SendBuffer[8] = 0x00; cUSART3SendBuffer[9] = 0x00; cUSART3SendBuffer[10] = 0x00; SendDataToUSART3(11); //发送数据 }
这里当时调试的发现,如果发送8个字节长度的数据,后面的两个字节不能正常的发出来,当时也没有多想,于是就多发了两个空字节的数据出来,以保证RS485的数据可以正常发出,当时调试的发现温控表返回的数据会有错误的情况:
如下图所示:
当时我还和厂家去沟通,厂家给我了一个上位机modbus软件进行测试,发现没有问题,当用给温度表加热时,数据是可以正常返回的,并没有温度响应不正常情况,我又开始怀疑SP3485的芯片问题,更换了SP3485芯片和MAX3485 两个厂家的硬件,发现问题依旧,我想这应该是我代码问题。
开始我并不确定该从何下手,去查找问题,但是我发现只有多发出两个字节长度,RS485芯片才能正常发出数据,会不会就是这里的原理,于是我有开始查找为什么会出现这样的稀奇古怪的问题;
于是我在网上查了下STM32中串口发送中断的相关的中断标志位:
1:USART_DR寄存器,这里相对应串口中断标志是USART_IT_TXE;只要USART_IT_TXE==1,就可以往USART_DR内传数据。
当USART_DR中的全部数据传送到移位寄存器后,此时USART_DR为空,USART_IT_TXE被设置为1,此时程序可以把下一个要发送的字节(操作USART_DR)可以写入USART_DR中。
2:移位寄存器,这里相对应串口中断标志是USART_IT_TC;只要USART_IT_TC==1,就可以往USART_DR内传数据。
当移位寄存器中的全部数据移出后,此时移位寄存器为空,USART_IT_TC被设置为1,此时程序可以把下一个要发送的字节(操作USART_DR)可以写入USART_DR中。
USART_IT_TC是移位寄存器把数据传输完后置1有效,只要把USART_IT_TC标志位置0就不再会进入中断
USART_IT_TXE是USART_DR寄存器为空就置1从而开启中断,所以一开始USART_DR寄存器没有数据时也会进入一下中断,因为只要寄存器空就进入中断所以USART_IT_TXE需要的是直接关掉中断,USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
查到这里我似乎懂了为什么要多发两个字节了。
于是我修改了下,串口3的中断处理函数:
if(USART_GetITStatus(USART3, USART_IT_TXE) != RESET) { temp3 = iUSART3SendLength; USART_SendData(USART3, cUSART3SendBuffer[iUSART3SendPoint++]); if(iUSART3SendPoint >= temp3) //发送完毕 { iUSART3SendPoint = 0; USART_ITConfig(USART3, USART_IT_TXE, DISABLE); USART_ITConfig(USART3, USART_IT_TC, ENABLE); // bUART3BusyFlag = FALSE; //发送完毕,复位忙标志 // GPIO_ResetBits(GPIOB, GPIO_Pin_12); //接受使能 } } else if(USART_GetITStatus(USART3, USART_IT_TC) != RESET) { USART_ITConfig(USART3, USART_IT_TC, DISABLE); USART_ClearFlag(USART3,USART_FLAG_TC); bUART3BusyFlag = FALSE; //发送完毕,复位忙标志 GPIO_ResetBits(GPIOB, GPIO_Pin_12); //接受使能 } else if(USART_GetFlagStatus(USART3, USART_FLAG_ORE) != RESET) temp3 = USART_ReceiveData(USART3); //依次读取USART_SR和USART_DR寄存器,可以清零ORE标志
当判断了移位寄存器中的数据为空时,再去将485芯片的接收引脚置为接收状态,同时修改了串口3接收函数的处理部分:
void FT807ModbusReadTemp(u8 Addr) { unsigned short ui16CRC; cUSART3SendBuffer[0] = Addr; cUSART3SendBuffer[1] = 0x04; cUSART3SendBuffer[2] = 0x00; cUSART3SendBuffer[3] = 0x00; cUSART3SendBuffer[4] = 0x00; cUSART3SendBuffer[5] = 0x04; ui16CRC = ClcCRC16(&cUSART3SendBuffer[0],6); cUSART3SendBuffer[6] = ui16CRC>>8; cUSART3SendBuffer[7] = ui16CRC; SendDataToUSART3(8); //发送数据 }
这里发送了8个字节长度的数据,发现可以正常发出了。然后用串口工具监测一下485的数据,发现通讯也正常了:
实物测试图如下所示:
避坑经验如下:
当时判断发送DR寄存器中断时候,错误就把485芯片的接收引脚置为接收状态,这时候串口中的数据并没有完全的发送出来,而是放到了移位寄存器里面,这也就是为什么会有两个字节长度的数据没有发出来的原因了,只有在触发了移位寄存器的中断时,再去将芯片的使能引脚置为接收状态就好了。然后分享下串口3的接收中断处理部分:
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) { temp3 = USART_ReceiveData(USART3); USART_ClearITPendingBit(USART3,USART_IT_RXNE); if(bUSART3_RecvOK==TRUE) //数据包未处理,不再接收 return; bUSART3OverTime = USART3OVT; //超时定时器 // 01 04 00 00 00 01 31 CA 01 04 02 01 00 B8 A0 // 01 06 00 00 03 E8 89 74 01 06 00 00 03 E8 89 74 switch(backState3) { case 0: if(temp3==0x01) { backState3 = 1; cUSART3RecvBuffer[0] =temp3 ; } else { backState3 = 0; } break; case 1: if((temp3==0x04) ||((temp3==0x03) ) ) { backState3 = 2; cUSART3RecvBuffer[1] =temp3 ; iUSART3RecvPoint =1 ; } else if(temp3==0x06) { backState3 = 3; cUSART3RecvBuffer[1] =temp3 ; iUSART3RecvPoint =1 ; } else { backState3 = 0; } break; case 2: iUSART3RecvPoint++; cUSART3RecvBuffer[iUSART3RecvPoint] = temp3; //01 04 00 00 00 04 F1 C9 01 04 08 01 01 00 00 00 00 00 00 F5 01 if(iUSART3RecvPoint >= 12) //已经接收到12个数据 { iUSART3RecvPoint = 0; backState3 = 0; DealTempData(); } break; case 3: iUSART3RecvPoint++; cUSART3RecvBuffer[iUSART3RecvPoint] = temp3; //01 04 00 00 00 04 F1 C9 01 04 08 01 01 00 00 00 00 00 00 F5 01 if(iUSART3RecvPoint >= 7) //已经接收到12个数据 { iUSART3RecvPoint = 0; backState3 = 0; DealTempData(); } break; default : break ; }
接收处理代码有些乱,稍后移植一下标准的modbus通讯的代码在和大家分享经验;
与之前写的接收固定长度数据,再在接收buffer里面找帧头的方式相比,只在串口中断中接收正确的数据,容错率要好一些。当然这种类似状态机的处理方式,编写代码比较啰嗦、麻烦。
要是使用RS232或者是TTL通讯方式,估计这里也不会有类似的问题,毕竟硬件方面没有使能引脚的控制方式。
这里还是要说明一下:当初的温控表并不支持标准的modbus协议,而是自有的AI协议,当时温控表回复的数据比较慢,所以也没有发现问题,后来厂家把温控表升级了一下,支持了标准modbus协议,返回的数据的时间做了提升,要不然也不会发现这里底层的驱动代码存在问题,看来搞软件要经常的维护代码。