什么是modbus?
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。
要注意的是::::::MODBUS协议是一种软件协议,是一种人为约定的协议,他和SPI,IIC,CAN总线协议还是有些不同的,SPI,IIC,CAN总线这些协议必须是设备在硬件上支持的,可以说SPI,IIC,CAN总线是一种软硬件的结合体,也就是常分为两层即物理层和协议层,MODBUS本身就是类似于协议层的东西。Modbus通信标准协议可以通过各种传输方式传播,如 RS232C、RS485、光纤、无线电等。
Modbus比其他通信协议使用的更广泛的主要原因有:
公开发表并且无版权要求
易于部署和维护
对供应商来说,修改移动本地的比特或字节没有很多限制
Modbus允许多个 (大约240个) 设备连接在同一个网络上进行通信,举个例子,一个测量温度和湿度的装置,并且将结果发送给计算机。在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。
Modbus协议大致分为以下两种串行传输模式:
Modbus-RTU
Modbus-ASCII
一个设备只会使用一种协议,一般来说大部分的设备都是Modbus-RTU协议。
设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是RTU,ASCII作为可选项。一般学习Modbus协议,只需要了解RTU协议,ASCll了解即可。
Modbus通讯过程
Modbus是一种单主站的主/从通信模式。,主机发送,从机应答,主机不发送,总线上就没有数据通信。
![]()
举例: 一个总线上有一个主机,多个从机,主机查询其中一个从机,首先得给这些从机分配地址(每个地址必须唯一),分配好地址后,主机先查询,然后发数据,从机得到主机发送的数据,然后对应地址的从机回复,主机得到从机数据。
注意:
Modbus不能判断从机是否忙,也没有对应的仲裁机制,我们只能通过软件对数据进行适当的处理!
帧结构
帧结构 = 地址 + 功能码+ 数据 + 校验
●地址: 占用一个字节,范围0-255,其中有效范围是1-247,其他有特殊用途。Modbus网络上只能有一个主站存在,主站在 Modbus网络上没有地址,从站的地址范围为 0 - 247,其中 0 为广播地址,从站的实际地址范围为 1 - 247。
●功能码:占用一个字节,功能码的意义就是,知道这个指令是干啥的,比如你可以查询从机的数据,也可以修改数据,所以不同功能码对应不同功能。
部分功能码:
其中,功能码03和06是比较常用的。
●数据:占用一个或多个字节,根据功能码不同,有不同结构。
●校验:为了保证数据不错误,增加这个,然后再把前面的数据进行计算看数据是否一致,如果一致,就说明这帧数据是正确的,我再回复;如果不一样,说明你这个数据在传输的时候出了问题,数据不对的,所以就抛弃了。
举例说明
我们大部分时候都是用modbus来和传感器通信。如果要查询传感器上的信息,用
03
查询功能码,如果需要修改传感器寄存器的值就用06
修改功能码,其他的不需要过多关注,用到的时候再去了解。
查询功能码
比如我们现在要使用STM32查询某传感器的数据,该传感器的地址为01。
主机发送: 01 03 00 00 00 01 84 0A
从机回复: 01 03 02 19 98 B2 7E什么意思?解析如下:
发送数据解析
01-地址,也就是你传感器的地址
03-功功能码,03代表查询功能,查询传感器的数据
00 00-代表查询的起始寄存器地址.说明从0x0000开始查询。这里需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据
00 01-代表查询了一个寄存器.结合前面的00 00,意思就是查询从0开始的1个寄存器值
84 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到84前面为止。回复数据解析
01-地址,也就是你传感器的地址
03-功功能码,03代表查询功能,查询传感器的数据。这里要注意的是注意发给从机的功能码是啥,从机就要回复同样的功能码,如果不一样说明这一帧数据有错误
02-代表后面数据的字节数,因为上面说到,一个寄存器有2个字节,所以后面的字节数肯定是2*查询的寄存器个数;
19 98-寄存器的值是19 98,结合发送的数据看出,01这个寄存器的值为19 98
B2 7E-循环冗余校验总结就是:
发送:从机的地址+我要干嘛的功能码+我要查的寄存器的地址+我要查的寄存器地址的个数+校验码
回复:从机的地址+主机发我的功能码+要发送给主机数据的字节数+数据+校验码
修改功能码
主机发送: 01 06 00 00 00 01 48 0A
从机回复: 01 06 00 00 00 01 48 0A看上去怎么一样的啊?是不是错了?答案是这是正确的。
发送数据解析
01-主机要查询的从机地址
06-功能码,06代表修改单个寄存器功能,修改有些不同,有修改一个寄存器和修改多个寄存器;
00 00-代表修改的起始寄存器地址.说明从0x0000开始.
00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止回复数据解析
01-从机返回给主机自己的地址,说明这就是主机查的从机
06-功能码,代表修改单个寄存器功能,主机发啥功能码,从机就必须回什么功能码;
00 00-代表修改的起始寄存器地址.说明是0x0000.
00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止;如果回复的一样,说明这个数据是修改成功的;如果功能码不是06,而是别的,说明从机回复的数据有误,主机可以做相应的处理。
如果我要修改多个寄存器,难道用06发好几次,这样不会太傻了吗?所以Modbus RTU协议包含了修改连续多个寄存器的方法,就是功能码为0x10;这个大家自己去查询,基本和上面的数据格式差不多。
注意,ModBus只是一个软件协议,传输时可以通过串口来进行通信。
为了提升传输效率,可以结合DMA使用。
MX配置
配置串口
配置DMA
发送和接收都采用DMA
开启空闲中断
关键代码
/* Includes ------------------------------------------------------------------*/ #include "MyApplication.h" /* Private define-------------------------------------------------------------*/ #define FunctionCode_Read_Register (uint8_t)0x03 #define FunctionCode_Write_Register (uint8_t)0x06 #define Modbus_Order_LENGTH (uint8_t)8 /* Private variables----------------------------------------------------------*/ /* Private function prototypes------------------------------------------------*/ static void Protocol_Analysis(UART_t*); //协议分析 static void Modbus_Read_Register(UART_t*); //读寄存器 static void Modbus_Wrtie_Register(UART_t*); //写寄存器 /* Public variables-----------------------------------------------------------*/ Modbus_t Modbus = { 1, Protocol_Analysis }; /* * @name Protocol_Analysis * @brief 协议分析 * @param UART -> 串口指针 * @retval None */ static void Protocol_Analysis(UART_t* UART) { UART_t* const COM = UART; uint8_t i = 0,Index = 0; //串口3停止DMA接收 HAL_UART_DMAStop(&huart3); //过滤干扰数据,首字节为modbus地址,共8字节 for(i=0;i<UART3_Rec_LENGTH;i++) { //检测键值起始数据Modbus.Addr if(Index == 0) { if(*(COM->pucRec_Buffer+i) != Modbus.Addr) continue; } *(COM->pucRec_Buffer+Index) = *(COM->pucRec_Buffer+i); //已读取7个字节 if(Index == Modbus_Order_LENGTH) break; Index++; } //计算CRC-16 CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucRec_Buffer,6); //计算CRC值 CRC_16.CRC_H = (uint8_t)(CRC_16.CRC_Value >> 8); CRC_16.CRC_L = (uint8_t)CRC_16.CRC_Value; //校验CRC-16 if(((*(COM->pucRec_Buffer+6) == CRC_16.CRC_L) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_H)) || ((*(COM->pucRec_Buffer+6) == CRC_16.CRC_H) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_L))) { //校验地址 if((*(COM->pucRec_Buffer+0)) == Modbus.Addr) { //处理数据 if((*(COM->pucRec_Buffer+1)) == FunctionCode_Read_Register) { Modbus_Read_Register(COM); } else if((*(COM->pucRec_Buffer+1)) == FunctionCode_Write_Register) { Modbus_Wrtie_Register(COM); } } } //清缓存 for(i=0;i<UART3_Rec_LENGTH;i++) { *(COM->pucRec_Buffer+i) = 0x00; } } /* * @name Modbus_Read_Register * @brief 读寄存器 * @param UART -> 串口指针 * @retval None */ static void Modbus_Read_Register(UART_t* UART) { UART_t* const COM = UART; //校验地址 if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x41)) { 回应数据 //地址码 *(COM->pucSend_Buffer+0) = Modbus.Addr; //功能码 *(COM->pucSend_Buffer+1) = FunctionCode_Read_Register; //数据长度 *(COM->pucSend_Buffer+2) = 8; //SHT30温度 *(COM->pucSend_Buffer+3) = ((uint16_t)((SHT30.fTemperature+40)*10))/256; *(COM->pucSend_Buffer+4) = ((uint16_t)((SHT30.fTemperature+40)*10))%256; //SHT30湿度 *(COM->pucSend_Buffer+5) = 0; *(COM->pucSend_Buffer+6) = SHT30.ucHumidity; //继电器状态 *(COM->pucSend_Buffer+7) = 0; *(COM->pucSend_Buffer+8) = Relay.Status; //蜂鸣器状态 *(COM->pucSend_Buffer+9) = 0; *(COM->pucSend_Buffer+10) = Buzzer.Status; //插入CRC CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucSend_Buffer,11); //计算CRC值 CRC_16.CRC_H = (uint8_t)(CRC_16.CRC_Value >> 8); CRC_16.CRC_L = (uint8_t)CRC_16.CRC_Value; *(COM->pucSend_Buffer+11) = CRC_16.CRC_L; *(COM->pucSend_Buffer+12) = CRC_16.CRC_H; //发送数据 UART3.SendArray(COM->pucSend_Buffer,13); } } /* * @name Modbus_Read_Register * @brief 写寄存器 * @param UART -> 串口指针 * @retval None */ static void Modbus_Wrtie_Register(UART_t* UART) { UART_t* const COM = UART; uint8_t i; 回应数据 //准备数据 for(i=0;i<8;i++) { *(COM->pucSend_Buffer+i) = *(COM->pucRec_Buffer+i); } //发送数据 UART3.SendArray(COM->pucSend_Buffer,8); //提取数据 //校验地址 -> 继电器 if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x43)) { //控制继电器 if(*(COM->pucRec_Buffer+5) == 0x01) { Relay.Relay_ON(); } else { Relay.Relay_OFF(); } } //校验地址 -> 蜂鸣器 if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x44)) { //控制蜂鸣器 if(*(COM->pucRec_Buffer+5) == 0x01) { Buzzer.ON(); } else { Buzzer.OFF(); } } } /******************************************************** End Of File ********************************************************/
CRC校验代码
/* Includes ------------------------------------------------------------------*/ #include "MyApplication.h" /* Private define-------------------------------------------------------------*/ /* Private variables----------------------------------------------------------*/ /* Private function prototypes------------------------------------------------*/ static uint16_t CRC_Check(uint8_t*,uint8_t); //CRC校验 /* Public variables-----------------------------------------------------------*/ CRC_16_t CRC_16 = {0,0,0,CRC_Check}; /******************************************************* 说明:CRC添加到消息中时,低字节先加入,然后高字 CRC计算方法: 1.预置1个16位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器; 2.把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低 8位相异或,把结果放于CRC寄存器; 3.把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位; 4.如果移出位为0:重复第3步(再次右移一位); 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或; 5.重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理; 6.重复步骤2到步骤5,进行通讯信息帧下一个字节的处理; 7.将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低 字节进行交换; ********************************************************/ /* * @name CRC_Check * @brief CRC校验 * @param CRC_Ptr->数组指针,LEN->长度 * @retval CRC校验值 */ uint16_t CRC_Check(uint8_t *CRC_Ptr,uint8_t LEN) { uint16_t CRC_Value = 0; uint8_t i = 0; uint8_t j = 0; CRC_Value = 0xffff; for(i=0;i<LEN;i++) { CRC_Value ^= *(CRC_Ptr+i); for(j=0;j<8;j++) { if(CRC_Value & 0x00001) CRC_Value = (CRC_Value >> 1) ^ 0xA001; else CRC_Value = (CRC_Value >> 1); } } CRC_Value = ((CRC_Value >> 8) + (CRC_Value << 8)); //交换高低字节 return CRC_Value; } /******************************************************** End Of File ********************************************************/