发新帖我要提问
12
返回列表
打印
[其他ST产品]

STM32作为从机通过RS485实现Modbus RTU通讯

[复制链接]
楼主: 我爱台妹mmd
手机看帖
扫描二维码
随时随地手机跟帖
21
我爱台妹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的具体实现参考下述代码部分。

使用特权

评论回复
22
我爱台妹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置于发送态,然后通过串口发送数据即可。

使用特权

评论回复
23
我爱台妹mmd|  楼主 | 2022-11-30 18:29 | 只看该作者
各部分代码:
1.串口代码:包括串口和I/O的初始化、串口中断、串口发送
//串口和I/O的初始化
void rs485_uart1_init(u32 bound)
{
  GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;       
       
        //外设的时钟使能
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);        //使能USART1时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //使能GPIO A 时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);   //使能GPIO B 时钟
       
       
        //GPIO参数设置
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA, &GPIO_InitStructure);
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA, &GPIO_InitStructure);        X
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
        GPIO_Init(GPIOB, &GPIO_InitStructure);       
       
        //串口参数设置
        USART_InitStructure.USART_BaudRate = bound;                                                       
        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(USART1, &USART_InitStructure);                               
       
        USART_ClearFlag(USART1, USART_FLAG_TC);
       
        //中断设置                 
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;      
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;                                                       
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);  
        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);                        
       
        USART_Cmd(USART1, ENABLE);                                        
        GPIO_ResetBits(RS485_TX_EN);                                 
}

//串口中断
void USART1_IRQHandler(void)
{
        u8 Res;
        if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //判断是否接收到数据
        {
                Res =USART_ReceiveData(USART1);//(USART1->DR);           //读取数据
                TIM_SetCounter(TIM2,0);
                TIM_Cmd(TIM2, ENABLE);   //开启计时器
                USART_RX_BUF[RS485_RX_CNT]=Res ;
                RS485_RX_CNT++;                            
  }
}

//串口发送
void RS485_Send_Data(u8 *buf,u8 len)
{
        u8 t;
        GPIO_SetBits(RS485_TX_EN);               
          for(t=0;t<len;t++)       
        {
          while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
    USART_SendData(USART1,buf[t]);
        }         
        while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);           
        GPIO_ResetBits(RS485_TX_EN);                         
}

使用特权

评论回复
24
我爱台妹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();
  }
}

使用特权

评论回复
25
我爱台妹mmd|  楼主 | 2022-11-30 18:30 | 只看该作者
CRC校验函数
u16 CRC_16( u8 *vptr, u8 len)
{
    uint16_t TCPCRC = 0xffff;
    uint16_t POLYNOMIAL = 0xa001;
    uint8_t i, j;

    for (i = 0; i < len; i++)
    {
        TCPCRC ^= vptr[i] ;
        for (j = 0; j < 8; j++)
        {
            if ((TCPCRC & 0x0001) != 0)
            {
                TCPCRC >>= 1;
                TCPCRC ^= POLYNOMIAL;
            }
            else
            {
                TCPCRC >>= 1;
            }
        }
    }
    return TCPCRC;
}

使用特权

评论回复
26
我爱台妹mmd|  楼主 | 2022-11-30 18:32 | 只看该作者
Modbus函数:包括Modbus处理函数和Modbus功能服务函数
//Modbus处理函数
void Modbus_Work(void)
{
        u8 t;
       
        if(*Flag_of_Modbus_Ok==1)
        {
    if(USART_RX_BUF[0]==0x03)       //判断设备地址码是否正确
          {
                  if((CRC_16(USART_RX_BUF,RS485_RX_CNT))==0x0000)    //判断CRC校验是否正确
                  {
                    switch(USART_RX_BUF[1])                      //根据功能码选择服务函数
                          {
                            case 0x03:                       //功能码03
            Modbus_03_Solve();
                                   break;
                                       
                                  case 0x10:          //功能码16
            Modbus_16_Solve();
                                  break;
                                        
                                                                       
          default:       //未定义的功能码
                                    USART_TX_BUF[0]=0x03;
                                    USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                                    USART_TX_BUF[2]=0X01;
                                    ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                                    RS485_Send_Data(USART_TX_BUF,5);                                                 
          break;       
                                       
                          }
                  }
                  else     //CRC校验不正确
                        {
                                USART_TX_BUF[0]=0x03;
                                USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                                USART_TX_BUF[2]=0X04;
                                ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                                RS485_Send_Data(USART_TX_BUF,5);                               
                        }
          }
          else memset(USART_RX_BUF,0,sizeof(USART_RX_BUF));     //非设备地址码,清除接收
           
          RS485_RX_CNT=0;
          *Flag_of_Modbus_Ok=0;
  }
}

//Modbus 03功能码处理函数
void Modbus_03_Solve(void)
{
        u8 t;
       
        if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
        {
                if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))
                {
                        USART_TX_BUF[0]=0x03;                 
                    USART_TX_BUF[1]=0x03;                  
                        USART_TX_BUF[2]=USART_RX_BUF[5]*2 ;   
                        for(t=0;t<(USART_TX_BUF[2]);t++)
                        {
                                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);
                        }
                        ((u16)*(USART_TX_BUF+(3+USART_TX_BUF[2])))=(CRC_16(USART_TX_BUF,3+USART_TX_BUF[2]));
                        RS485_Send_Data(USART_TX_BUF,5+USART_TX_BUF[2]);                                                       
                }
                else
                {
                        USART_TX_BUF[0]=0x03;
                        USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                        USART_TX_BUF[2]=0x02;
                        ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                        RS485_Send_Data(USART_TX_BUF,5);                                                                               
                }
        }
        else  
        {
          USART_TX_BUF[0]=0x03;
                USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                USART_TX_BUF[2]=0x02;
                ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                RS485_Send_Data(USART_TX_BUF,5);                                               
        }       
       
}


//Modbus 16功能码服务函数
void Modbus_16_Solve(void)
{
        u8 t;

        if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
        {
                if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))  
                {
                  for(t=0;t<USART_RX_BUF[5];t=t+2)
                  {
                          work_register[USART_RX_BUF[3]+(t/2)]=USART_RX_BUF[6+t]*256+USART_RX_BUF[7+t];
                        }                                                       
                        USART_TX_BUF[0]=0x03;               
                        USART_TX_BUF[1]=0x10;                  
                        USART_TX_BUF[2]=USART_RX_BUF[2];     
                        USART_TX_BUF[3]=USART_RX_BUF[3];      
                        USART_TX_BUF[4]=USART_RX_BUF[4];     
                        USART_TX_BUF[5]=USART_RX_BUF[5];                                  
                        ((u16)*(USART_TX_BUF+6))=CRC_16(USART_TX_BUF,6);
                        RS485_Send_Data(USART_TX_BUF,8);                                                       
                }
                else  
                {
            USART_TX_BUF[0]=0x03;
                  USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                  USART_TX_BUF[2]=0x02;
            ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                  RS485_Send_Data(USART_TX_BUF,5);                                                                       
                }
        }  
        else  
        {
          USART_TX_BUF[0]=0x03;
                USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
                USART_TX_BUF[2]=0x02;
                ((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
                RS485_Send_Data(USART_TX_BUF,5);                                       
        }       
       
}

使用特权

评论回复
27
我爱台妹mmd|  楼主 | 2022-11-30 18:33 | 只看该作者
MODBUS协议调试时出现的问题和原因
1.STM32采集到的数据帧的位数和主机发送的数据帧的位数一致,但是接收到的数据内容为FF FE EF类似的乱码。
原因:有可能是RS485的A、B线接反,一般来说,A接A、B接B,但是不排除有些板子内部接反了。此外,RS485中A、B线的电压不稳定等原因也有可能造成乱码的存在。

使用特权

评论回复
28
我爱台妹mmd|  楼主 | 2022-11-30 18:34 | 只看该作者
2.STM32可以收到数据,但是没有数据发出。
原因:有可能是RS485的使能I/O没有起作用。

使用特权

评论回复
29
我爱台妹mmd|  楼主 | 2022-11-30 18:34 | 只看该作者
确定主机有发数据出来,但是STM32无数据接收。
原因:检查是否是STM32的USART TX和RX和RS485芯片的接收和发送端接反了。

使用特权

评论回复
30
我爱台妹mmd|  楼主 | 2022-11-30 18:35 | 只看该作者
更多问题等待实践的进一步探索,未完待续)

使用特权

评论回复
31
我爱台妹mmd|  楼主 | 2022-11-30 18:36 | 只看该作者
很多人想要源码,其实上面的所有代码整合在一起就是源码,至于我做项目时用的代码,由于和项目相关的代码段太多,涉及到的专用的硬件是西门子的PLC的MODBUS通讯,而且自己写的代码注释做的不是很好(现在也懒得回去加注释),反而需要读者自行从大量的驱动代码中提取出来MODBUS代码,还不如之间用上面的代码组合成一个框架,然后自己在里面添加和自己应用相关的程序。

使用特权

评论回复
32
Wordsworth| | 2024-11-8 07:05 | 只看该作者

主从定时的方式占用CPU资源少

使用特权

评论回复
33
Clyde011| | 2024-11-8 08:08 | 只看该作者

主从定时器门控的方式

使用特权

评论回复
34
公羊子丹| | 2024-11-8 09:01 | 只看该作者

主定时器为TIM1,通道2配置为PWM输出

使用特权

评论回复
35
万图| | 2024-11-8 10:04 | 只看该作者

中断计数的方式实现简

使用特权

评论回复
36
Uriah| | 2024-11-8 11:07 | 只看该作者

当PWM频率较高时,频繁的中断将影响程序运行的效率

使用特权

评论回复
37
帛灿灿| | 2024-11-8 13:03 | 只看该作者

都可以产生指定个数的PWM脉冲

使用特权

评论回复
38
Bblythe| | 2024-11-8 14:06 | 只看该作者

输出了5个频率为10KHz的PWM脉冲

使用特权

评论回复
39
周半梅| | 2024-11-8 16:02 | 只看该作者

从定时器为TIM2,从模式选择为门控模式,触发源选择ITR0,开启定时器2中断。

使用特权

评论回复
40
Pulitzer| | 2024-11-8 17:05 | 只看该作者

根据实际需求选择用哪种方式

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则