[其他ST产品] STM32作为从机通过RS485实现Modbus RTU通讯

[复制链接]
2712|30
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:27 | 显示全部楼层
Modbus校验码:
Modbus一般采用的16位的CRC校验。什么是CRC校验呢,简单来说,比如你要发一段数据,最后想在后面加两个字节,这两个字节是通过前面所发的数据通过某种算法计算出来的唯一的两个字节。如果发送过程中无问题,那么接收端通过相同的算法能够得到同样的最后两位字节,这就表明该帧的所有的数据都是正确的。如果接收端通过同样的算法算出来的两个字节数据和发送过来的最后两个字节不同,那么就表明在发送和接收过程中,有些数据接收或发送错误。通过校验位来判断该帧是否正确发送和接收。
CRC就是其中一种校验方法,其算法是将所发的数据左移一定位数(例如Modbus就是左移16位,从而空出两个字节)后,与一个约定好的17位二进制数进行模2除法(例如Modbus就是 1 1000 0000 0000 0101),最后得到的余数就是我们所需要的校验码。接收端在接收到数据帧后,把整个带有校验码的数据进行同样的操作,最后得到的余数为0,则表明校验码正确,如果不为0,则表明校验码出错。
具体模2除法是什么东西,简单来说,就是对除数和被除数进行按位异或,然后除数移位,然后两个进行异或,然后除数移位,如此循环,直到除数和被除数位数相同,结束运算。具体过程可以参考被人写的博客,例如:https://blog.csdn.net/tjd10061/article/details/48808633
CRC的具体实现参考下述代码部分。
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:28 | 显示全部楼层
STM32代码实现
5.1 代码的总体框架:
STM32实现RS485的Modbus通讯过程。根据上述我们对串口、485、Modbus的讲解,这里我们会用到STM32的串口功能(用于收发数据)、I/O功能(用于使能和失能485的收发)、定时器功能(用于对接收的数据的间隔进行计时,以判断数据帧是否接收完成)、CRC功能(进行CRC校验)以及Modbus的服务函数。
整体的代码框架为:如果STM32作为从机,在进行串口初始化后,通过I/O使485常态处于接收态,并采用串口中断读取接收到的每一个字节,在每次接收字节时,开启计时器,如果计时器计时溢出,表明时间间隔大于3.5个字节接收时间,即一帧接收完成,此时进入计时器中断,可以进行Modbus的处理函数。Modbus的处理函数首先会判断设备是否是该设备,如果不是,则直接结束处理。如果是,则会进行CRC校验,如果CRC校验正确,则根据不同的功能码进行不同的服务函数。如果CRC校验不正确,则返回相应的错误代码。
当STM32需要发送数据时,先把485置于发送态,然后通过串口发送数据即可。
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:29 | 显示全部楼层
各部分代码:
1.串口代码:包括串口和I/O的初始化、串口中断、串口发送
  1. //串口和I/O的初始化
  2. void rs485_uart1_init(u32 bound)
  3. {
  4.   GPIO_InitTypeDef GPIO_InitStructure;
  5.         USART_InitTypeDef USART_InitStructure;
  6.         NVIC_InitTypeDef NVIC_InitStructure;       
  7.        
  8.         //外设的时钟使能
  9.         RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);        //使能USART1时钟
  10.         RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //使能GPIO A 时钟
  11.         RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);   //使能GPIO B 时钟
  12.        
  13.        
  14.         //GPIO参数设置
  15.         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  16.         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  17.         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  18.         GPIO_Init(GPIOA, &GPIO_InitStructure);
  19.         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  20.         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  21.         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  22.         GPIO_Init(GPIOA, &GPIO_InitStructure);        X
  23.         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  24.         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  25.         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
  26.         GPIO_Init(GPIOB, &GPIO_InitStructure);       
  27.        
  28.         //串口参数设置
  29.         USART_InitStructure.USART_BaudRate = bound;                                                       
  30.         USART_InitStructure.USART_WordLength = USART_WordLength_8b;        
  31.         USART_InitStructure.USART_StopBits = USART_StopBits_1;                         
  32.         USART_InitStructure.USART_Parity = USART_Parity_No ;                                        
  33.         USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  34.         USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  35.         USART_Init(USART1, &USART_InitStructure);                               
  36.        
  37.         USART_ClearFlag(USART1, USART_FLAG_TC);
  38.        
  39.         //中断设置                 
  40.         NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  41.         NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;      
  42.         NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;                                                       
  43.         NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  44.         NVIC_Init(&NVIC_InitStructure);  
  45.         USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);                        
  46.        
  47.         USART_Cmd(USART1, ENABLE);                                        
  48.         GPIO_ResetBits(RS485_TX_EN);                                 
  49. }

  50. //串口中断
  51. void USART1_IRQHandler(void)
  52. {
  53.         u8 Res;
  54.         if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //判断是否接收到数据
  55.         {
  56.                 Res =USART_ReceiveData(USART1);//(USART1->DR);           //读取数据
  57.                 TIM_SetCounter(TIM2,0);
  58.                 TIM_Cmd(TIM2, ENABLE);   //开启计时器
  59.                 USART_RX_BUF[RS485_RX_CNT]=Res ;
  60.                 RS485_RX_CNT++;                            
  61.   }
  62. }

  63. //串口发送
  64. void RS485_Send_Data(u8 *buf,u8 len)
  65. {
  66.         u8 t;
  67.         GPIO_SetBits(RS485_TX_EN);               
  68.           for(t=0;t<len;t++)       
  69.         {
  70.           while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
  71.     USART_SendData(USART1,buf[t]);
  72.         }         
  73.         while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);           
  74.         GPIO_ResetBits(RS485_TX_EN);                         
  75. }

 楼主| 我爱台妹mmd 发表于 2022-11-30 18:29 | 显示全部楼层
定时器代码:定时器的初始化、定时器中断
//定时器初始化
void TIM2_Init(u16 arr,u16 psc)  //PSC=7200-1  一次计数0.1ms
{   
        TIM_TimeBaseInitTypeDef   TIM_TimeBaseStructure;
  NVIC_InitTypeDef          NVIC_InitStructure;
       
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);  //TIMER2使能
       
        TIM_TimeBaseStructure.TIM_Period = arr;
        TIM_TimeBaseStructure.TIM_Prescaler =psc;
        TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
        TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
        TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
        TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE );
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

        NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);  
        TIM_SetCounter(TIM2,0);
       
        TIM_Cmd(TIM2, DISABLE);  
}

//定时器中断
void TIM2_IRQHandler()
{   
        if(TIM_GetITStatus(TIM2, TIM_IT_Update)!=RESET)      
  {   
                TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
                TIM_Cmd(TIM2, DISABLE);  //¹Ø±ÕʱÖÓ
        //        LED_Change();
                *Flag_of_Modbus_Ok=1;
                Modbus_Work();
  }
}
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:30 | 显示全部楼层
CRC校验函数
  1. u16 CRC_16( u8 *vptr, u8 len)
  2. {
  3.     uint16_t TCPCRC = 0xffff;
  4.     uint16_t POLYNOMIAL = 0xa001;
  5.     uint8_t i, j;

  6.     for (i = 0; i < len; i++)
  7.     {
  8.         TCPCRC ^= vptr[i] ;
  9.         for (j = 0; j < 8; j++)
  10.         {
  11.             if ((TCPCRC & 0x0001) != 0)
  12.             {
  13.                 TCPCRC >>= 1;
  14.                 TCPCRC ^= POLYNOMIAL;
  15.             }
  16.             else
  17.             {
  18.                 TCPCRC >>= 1;
  19.             }
  20.         }
  21.     }
  22.     return TCPCRC;
  23. }
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:32 | 显示全部楼层
Modbus函数:包括Modbus处理函数和Modbus功能服务函数
  1. //Modbus处理函数
  2. void Modbus_Work(void)
  3. {
  4.         u8 t;
  5.        
  6.         if(*Flag_of_Modbus_Ok==1)
  7.         {
  8.     if(USART_RX_BUF[0]==0x03)       //判断设备地址码是否正确
  9.           {
  10.                   if((CRC_16(USART_RX_BUF,RS485_RX_CNT))==0x0000)    //判断CRC校验是否正确
  11.                   {
  12.                     switch(USART_RX_BUF[1])                      //根据功能码选择服务函数
  13.                           {
  14.                             case 0x03:                       //功能码03
  15.             Modbus_03_Solve();
  16.                                    break;
  17.                                        
  18.                                   case 0x10:          //功能码16
  19.             Modbus_16_Solve();
  20.                                   break;
  21.                                         
  22.                                                                        
  23.           default:       //未定义的功能码
  24.                                     USART_TX_BUF[0]=0x03;
  25.                                     USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  26.                                     USART_TX_BUF[2]=0X01;
  27.                                     ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  28.                                     RS485_Send_Data(USART_TX_BUF,5);                                                 
  29.           break;       
  30.                                        
  31.                           }
  32.                   }
  33.                   else     //CRC校验不正确
  34.                         {
  35.                                 USART_TX_BUF[0]=0x03;
  36.                                 USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  37.                                 USART_TX_BUF[2]=0X04;
  38.                                 ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  39.                                 RS485_Send_Data(USART_TX_BUF,5);                               
  40.                         }
  41.           }
  42.           else memset(USART_RX_BUF,0,sizeof(USART_RX_BUF));     //非设备地址码,清除接收
  43.            
  44.           RS485_RX_CNT=0;
  45.           *Flag_of_Modbus_Ok=0;
  46.   }
  47. }

  48. //Modbus 03功能码处理函数
  49. void Modbus_03_Solve(void)
  50. {
  51.         u8 t;
  52.        
  53.         if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
  54.         {
  55.                 if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))
  56.                 {
  57.                         USART_TX_BUF[0]=0x03;                 
  58.                     USART_TX_BUF[1]=0x03;                  
  59.                         USART_TX_BUF[2]=USART_RX_BUF[5]*2 ;   
  60.                         for(t=0;t<(USART_TX_BUF[2]);t++)
  61.                         {
  62.                                 USART_TX_BUF[3+t]=(t%2==0)?(work_register[USART_RX_BUF[3]+(t/2)]/256):(work_register[USART_RX_BUF[3]+(t/2)]%256);
  63.                         }
  64.                         ((u16)*(USART_TX_BUF+(3+USART_TX_BUF[2])))=(CRC_16(USART_TX_BUF,3+USART_TX_BUF[2]));
  65.                         RS485_Send_Data(USART_TX_BUF,5+USART_TX_BUF[2]);                                                       
  66.                 }
  67.                 else
  68.                 {
  69.                         USART_TX_BUF[0]=0x03;
  70.                         USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  71.                         USART_TX_BUF[2]=0x02;
  72.                         ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  73.                         RS485_Send_Data(USART_TX_BUF,5);                                                                               
  74.                 }
  75.         }
  76.         else  
  77.         {
  78.           USART_TX_BUF[0]=0x03;
  79.                 USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  80.                 USART_TX_BUF[2]=0x02;
  81.                 ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  82.                 RS485_Send_Data(USART_TX_BUF,5);                                               
  83.         }       
  84.        
  85. }


  86. //Modbus 16功能码服务函数
  87. void Modbus_16_Solve(void)
  88. {
  89.         u8 t;

  90.         if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
  91.         {
  92.                 if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))  
  93.                 {
  94.                   for(t=0;t<USART_RX_BUF[5];t=t+2)
  95.                   {
  96.                           work_register[USART_RX_BUF[3]+(t/2)]=USART_RX_BUF[6+t]*256+USART_RX_BUF[7+t];
  97.                         }                                                       
  98.                         USART_TX_BUF[0]=0x03;               
  99.                         USART_TX_BUF[1]=0x10;                  
  100.                         USART_TX_BUF[2]=USART_RX_BUF[2];     
  101.                         USART_TX_BUF[3]=USART_RX_BUF[3];      
  102.                         USART_TX_BUF[4]=USART_RX_BUF[4];     
  103.                         USART_TX_BUF[5]=USART_RX_BUF[5];                                  
  104.                         ((u16)*(USART_TX_BUF+6))=CRC_16(USART_TX_BUF,6);
  105.                         RS485_Send_Data(USART_TX_BUF,8);                                                       
  106.                 }
  107.                 else  
  108.                 {
  109.             USART_TX_BUF[0]=0x03;
  110.                   USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  111.                   USART_TX_BUF[2]=0x02;
  112.             ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  113.                   RS485_Send_Data(USART_TX_BUF,5);                                                                       
  114.                 }
  115.         }  
  116.         else  
  117.         {
  118.           USART_TX_BUF[0]=0x03;
  119.                 USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
  120.                 USART_TX_BUF[2]=0x02;
  121.                 ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
  122.                 RS485_Send_Data(USART_TX_BUF,5);                                       
  123.         }       
  124.        
  125. }

 楼主| 我爱台妹mmd 发表于 2022-11-30 18:33 | 显示全部楼层
MODBUS协议调试时出现的问题和原因
1.STM32采集到的数据帧的位数和主机发送的数据帧的位数一致,但是接收到的数据内容为FF FE EF类似的乱码。
原因:有可能是RS485的A、B线接反,一般来说,A接A、B接B,但是不排除有些板子内部接反了。此外,RS485中A、B线的电压不稳定等原因也有可能造成乱码的存在。
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:34 | 显示全部楼层
2.STM32可以收到数据,但是没有数据发出。
原因:有可能是RS485的使能I/O没有起作用。
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:34 | 显示全部楼层
确定主机有发数据出来,但是STM32无数据接收。
原因:检查是否是STM32的USART TX和RX和RS485芯片的接收和发送端接反了。
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:35 | 显示全部楼层
更多问题等待实践的进一步探索,未完待续)
 楼主| 我爱台妹mmd 发表于 2022-11-30 18:36 | 显示全部楼层
很多人想要源码,其实上面的所有代码整合在一起就是源码,至于我做项目时用的代码,由于和项目相关的代码段太多,涉及到的专用的硬件是西门子的PLC的MODBUS通讯,而且自己写的代码注释做的不是很好(现在也懒得回去加注释),反而需要读者自行从大量的驱动代码中提取出来MODBUS代码,还不如之间用上面的代码组合成一个框架,然后自己在里面添加和自己应用相关的程序。
Wordsworth 发表于 2024-11-8 07:05 | 显示全部楼层

主从定时的方式占用CPU资源少
Clyde011 发表于 2024-11-8 08:08 | 显示全部楼层

主从定时器门控的方式
公羊子丹 发表于 2024-11-8 09:01 | 显示全部楼层

主定时器为TIM1,通道2配置为PWM输出
万图 发表于 2024-11-8 10:04 | 显示全部楼层

中断计数的方式实现简
Uriah 发表于 2024-11-8 11:07 | 显示全部楼层

当PWM频率较高时,频繁的中断将影响程序运行的效率
帛灿灿 发表于 2024-11-8 13:03 | 显示全部楼层

都可以产生指定个数的PWM脉冲
Bblythe 发表于 2024-11-8 14:06 | 显示全部楼层

输出了5个频率为10KHz的PWM脉冲
周半梅 发表于 2024-11-8 16:02 | 显示全部楼层

从定时器为TIM2,从模式选择为门控模式,触发源选择ITR0,开启定时器2中断。
Pulitzer 发表于 2024-11-8 17:05 | 显示全部楼层

根据实际需求选择用哪种方式
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部