【STM32】STM32F407实现简单的Modbus协议

前言

    这篇文章是我从零认识MODBUS过程中的一点笔记,主要讲解了我学习和使用MODBUS的思路。代码可能帮不到你,但是如果你是和我一样的初学者,建议你认真阅读。毕竟学习是一个思考练习的过程,如果只会Ctrl+C,那么就没什么意义了。作者水平有限,有错误敬请指出,互相学习,共同进步。

1.ModBus协议简介

       MODBUS协议是一种串行通信协议,由Modicon公司(施耐德公司前身)发表,由于其公开发表且无版权要求,易于部署和维护,在工业界广泛应用。MODBUS采用主从通信(Master/Slave),MODBUS有三种报文格式:ASCII、RTU、TCP,本文主要讨论RTU。

       如下图所示,串行通信上的MODBUS协议主要由地址,功能码,数据,CRC校验四部分数据帧构成。主机和从机的串行通信设备要求一致,参数要求一致,即假设使用串口,主从机波特率,奇偶校验等参数需一致。

 

【STM32】STM32F407实现简单的Modbus协议_第1张图片

 主机端状态图(标准流程)

【STM32】STM32F407实现简单的Modbus协议_第2张图片

从机端状态图(标准流程)

【STM32】STM32F407实现简单的Modbus协议_第3张图片

RTU报文数据格式:

从机地址:1 byte      功能码:1byte   数据:0-252byte(s)     CRC校验:2 byte  低位在前,高位在后                                                                                                                                                                    

【STM32】STM32F407实现简单的Modbus协议_第4张图片

2.MODBUS_RTU在STM32上的实现

    撸代码之前,我们先来看看MODBUS的数据模型,见下图。因为MODBUS通常用在PLC上,所以他的数据模型与PLC息息相关。这里我们不用管。直接看我给出的表格,便于理解,离散量输入即只读的位类型变量,与开关量一样。比如从机接了一个行程开关(按钮),我要读取开关的状态(只有0和1),而我们是无法直接操作开关的。再看"线圈",我的从机接了一个LED灯,我可以直接操作IO口控制灯的亮灭,也可以查询IO口电平知道灯的亮灭,所以他是可以读写的数据。"输入寄存器"理解为从机设备上有个温度传感器,传感器的数值是外界的温度,我们只能读取这个温度,所以是只读变量。"保持寄存器"理解为类似于从机ID一类信息,既可以被读取,也可以被改写。

【STM32】STM32F407实现简单的Modbus协议_第5张图片

【STM32】STM32F407实现简单的Modbus协议_第6张图片

   万事俱备,只欠东风,开搞。 我用的两块STM32F407的板子,一主一从,主机上什么外设也没有,假设从机上接了一个温度传感器,8个LED灯,以及8个按键。我们来写一个主机读取从机按键状态的MODBUS协议。

  2.1 功能码举例

       这里以功能码0x01为例,说明如何运用。完整的RTU报文读取线圈数据帧格式如下图所示。我们就主机端和从机端分别来写这个功能码。(其他功能码可以查看参考文献中的国标)

【STM32】STM32F407实现简单的Modbus协议_第7张图片

      主机端:向从机端请求线圈状态。根据上面的图片(参考国标中功能码0x01的说明)。主机向从机请求线圈状态时,发送的一帧数据包括从机地址,功能码,读取线圈的起始地址,读取线圈的数量。以及CRC校验码。按照这个思路,我们写一个void Read_Coil_State(uint8_t addr, uint16_t addr_read,uint16_t num)函数,它的参数从机地址,线圈地址以及线圈数量。

/* 函数名: 读线圈指令
 * 参  数: -*-addr       从机地址
           -*-addr_read   起始地址 
           -*-num         线圈数量
 * 返回值: None
 * 描  述: 向从机发送读线圈状态请求
*/
void Read_Coil_State(uint8_t addr, uint16_t addr_read,uint16_t num)
{
    uint8_t i;
    uint8_t Read_Coil[8];
    if(num >2000){
      return;}
    Read_Coil[0] = addr;
    Read_Coil[1] = 0x01;
    Read_Coil[2] = addr_read>>8;  //高位在前,低位在后
    Read_Coil[3] = addr_read;
    Read_Coil[4] = num>>8;
    Read_Coil[5] = num;
    Read_Coil[6] = CRC16(Read_Coil,4);
    Read_Coil[7] = CRC16(Read_Coil,4)>>8;
    for(i=0;i<=7;i++)
    {
      Uart_SendByte(Read_Coil[i]);  //串口发送
    }
}

        其中CRC16()是CRC校验函数,需要进行校验的数据包含地址码、功能码、数据,也就是CRC前面的堆数据都需要校验。关于CRC检验的具体原理,读者可以自行研究。这里采用的是官方推荐的查表校验法。下面的表格分别是CRC低位和CRC高位。最下面是CRC16()函数本身。

High-Order Byte Table
/* Table of CRC values for high–order byte */
static unsigned char auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40
} ;
Low-Order Byte Table
/* Table of CRC values for low–order byte */
static char auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40
};
unsigned short CRC16 (unsigned char *puchMsg, unsigned short usDataLen) /* The function returns the CRC as a unsigned short type */
{  
 /* message to calculate CRC upon */
/* quantity of bytes in message */
  {
  unsigned char uchCRCHi = 0xFF ; /* high byte of CRC initialized */
  unsigned char uchCRCLo = 0xFF ; /* low byte of CRC initialized */
  unsigned uIndex ; /* will index into CRC lookup table */
  while (usDataLen--) /* pass through message buffer */
  {
  uIndex = uchCRCLo ^ *puchMsg++ ; /* calculate the CRC */
  uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;
  uchCRCHi = auchCRCLo[uIndex] ;
  }
  return (uchCRCHi << 8 | uchCRCLo) ;
  }
}
}

       主机端就完成了,接下来写一个从机端MODBUS的线圈状态响应函数。从机在确认自己的地址是主机请求的地址之后,会将自己指定地址的线圈状态打包成数据发回主机。下面是从机端返回线圈状态的函数。

/* 函数名: 返回线圈状态
 * 参  数: None
 * 返回值: None
 * 描  述: 将线圈状态打包发给主机
*/

#define MAX_CoilState_LEN  255
uint8_t Send_Buffer[MAX_CoilState_LEN];
void Read_Coil_Response(void)
{
  uint8_t i,j;
  uint16_t num;
  uint16_t addr;
  uint16_t crc,crc_send;
  addr = (USART_RX_BUF[2]<<8) + USART_RX_BUF[3];  //获取地址
  num =  ((USART_RX_BUF[4]<<8) + USART_RX_BUF[5]); //字节数,这里应该是向上取整,读者自行研究
  crc = CRC16(USART_RX_BUF,8);  //CRC校验
//  if(addr > 0xffff || num>2000 || crc!= (USART_RX_BUF[6]<<8) + USART_RX_BUF[7]) {return;}   //返回异常码0x03数据值无效
//   
    Send_Buffer[i++] = ADDR_SELF; //本机地址
    Send_Buffer[i++] = 0x01;    // 功能码
    Send_Buffer[i++] = num;  // 字节数
  // 示例:读取并将线圈状态打包-多个字节
  //    for(i=3;i<3+num;i++)   
  //    {
  //      Send_Buffer[i] = Coil_Get();
  //    }
    Send_Buffer[i++] = 0xec;  // 线圈状态
  
    crc_send = CRC16(Send_Buffer,i); // 计算CRC
    Send_Buffer[i++] = crc_send; // CRC低位
    Send_Buffer[i++] = crc_send>>8;// CRC高位
    for(j=0;j

       主机和从机写完了,我们还需要在从机的串口中断中调用ModBus事务状态处理函数,因为不同的功能码有不同的处理函数,由下图的流程图,我给出了一个简单的例子,因为只写了读线圈这个功能码,所以只有一个调用。

【STM32】STM32F407实现简单的Modbus协议_第8张图片

u8 error_code;
void Modbus_Rec_Process(void)
{
  //地址不正确,丢弃,不予回应
  if(USART_RX_BUF[0] != ADDR_SELF)
  {
    return;
  }
  switch (USART_RX_BUF[1]){
    case 0x01:{        // 读线圈
                Read_Coil_Response();
    }break;
    
        case 0x02:{    //
    }break;
        
        case 0x03:{
    }break;
        
        case 0x04:{
    }break;
        
        case 0x05:{
    }break;
        
        case 0x06:{
    }break;
        
        case 0x0f:{
    }break;
        
        case 0x10:{
    }break;
        default: 
          error_code = 0x01;  // 无效功能码,返回错误码0x01
          //break;  //
    }
}

       下面是我的串口中断函数,使用了空闲接收。空闲接收可以一帧一帧的接收数据,MODBUS自己没带帧头帧尾,只想到这个办法。


	//串口1中断服务程序
uint16_t length;
void USART1_IRQHandler(void)                
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		USART_RX_BUF[length++] =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数	
	} 
  else if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)   //接收到一帧数据
  {
    USART1->SR;//先读SR
    USART1->DR;//再读DRUSART1->SR;//先读SR
    length = 0; //一帧数据完,索引归零
    USART_IDLE_STA = 1;
    Modbus_Rec_Process();//调用modbus事务处理函数
  }
}

3.实验结果 

       这里使用的是Modbus的调试工具ModScan32,在各大网站都可以下载到,打开软件连接串口,按照图示配置,我在例子里默认回复8个位的状态信息,且状态为0xec,可以看到软件读取到了正确的数据。说明我们的Modbus协议成功。

【STM32】STM32F407实现简单的Modbus协议_第9张图片

参考文献:

【1】.GBZ 19582.1-2004 基于Modbus协议的工业自动化网络规范 第1部分:Modbus应用协议

【2】.Modbus_over_serial_line_V1_02(www.modbus.org) 

【3】.modbus协议中的寄存器理解(https://blog.csdn.net/bijinsong/article/details/79373621)

【4】.串口接收不定长数据的几种方式(https://blog.csdn.net/zb774095236/article/details/82781749)

 

你可能感兴趣的:(STM32单片机)