这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【分享开发笔记,赚取电动螺丝刀】使用STM32F103ZE主控调试RS485通讯

共4条 1/1 1 跳转至

【分享开发笔记,赚取电动螺丝刀】使用STM32F103ZE主控调试RS485通讯的避坑经验

工程师
2025-05-05 20:54:35     打赏

最近项目上面的使用的温控表进行的产品的迭代升级,导致之前写的串口通讯部分代码出现了问题,这里和大家分享一些,在工作中的经验分享

一:硬件图纸如下所示:

0505-7.png

这里设计的时候选用的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的数据可以正常发出,当时调试的发现温控表返回的数据会有错误的情况:

如下图所示:

0505-8.png

当时我还和厂家去沟通,厂家给我了一个上位机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的数据,发现通讯也正常了:

0505-9.png

实物测试图如下所示:

0505-10.png

避坑经验如下:

当时判断发送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协议,返回的数据的时间做了提升,要不然也不会发现这里底层的驱动代码存在问题,看来搞软件要经常的维护代码。


专家
2025-05-06 09:07:33     打赏
2楼

谢谢分享


院士
2025-05-09 11:23:22     打赏
3楼

这可是实战经验总结与分享。

谢谢楼主的分享了


院士
2025-05-09 17:41:00     打赏
4楼

嗯嗯,这个问题在STM32后续设计上是有优化的,而且效果还是不错的。


共4条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]