目录
一、本文讨论内容
二、工具与源码
三、Modbus概述
四、Modbus-RTU通讯协议
五、完成Modbus输入输出代码
六、完成Modbus逻辑功能
七、测试与验证
一、本文讨论内容
本文简明概要的介绍Modbus通讯协议,并在STM32上实现Modbus协议,通过与Modbus Poll(一种Modbus上位机工具)联合调试以验证程序的正确性。
本文B站有视频讲解!均为作者原创。up主名字:“芯心智库”。
视频链接:芯心智库的个人空间_哔哩哔哩_bilibili
二、工具与源码
工具与源码下载方式:微信公众号关注“芯心智库”,回复“modbus”即可获取。
三、Modbus概述
Modbus是主从方式通信,一个总线上只有一个主机,但可以有多个从机(这一点类似于IIC的通讯方式)。简单来说,也就是主机问从机答。问啥呢?主要问两个问题--从机的寄存器(线圈)的数值是多少、主机要求从机修改一下寄存器(线圈)的数值并回答。
Modbus还可以细分很多种协议,但是主要就是四种:
1、Modbus-RTU(设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是RTU)
2、Modbus-ASCII(人类可读的,冗长的表示方式)
3、Modbus-TCP(以太网作为介质进行传输)
4、Modbus-PLUS(高速现场总线网络)
其实从本质来讲,这四个都是差不多的,你只要掌握一种,其余三种都可以轻而易举地掌握。本文就讲解Modbus-RTU协议,这个是最常使用的协议。
四、Modbus-RTU通讯协议
结合前面所讲,再来理解一下Modbus通讯协议,我们可以把通讯过程理解为主机叫哪个从机,做什么事情,怎么做,最后主机检查自己描述的对不对。一共四个步骤,也就是每次通讯都需要包含这四个步骤,每一次通讯都需要发送一个帧结构。所以就有 帧结构 = 地址 + 功能码 + 数据 + CRC校验。地址就是相当于哪个从机,功能码就是要干的事情,数据就是要具体怎么做,最后来个CRC校验确保数据是对的。值得注意的是地址: 地址的有效范围是1-247,其他有特殊用途,比如255是广播地址(广播地址就是应答所有地址,正常的需要两个设备的地址一样才能进行查询和回复),而且RTU没有帧头和帧尾,所以协议里明确两帧之间要大于3.5个字节时间间隔,作为一帧结束的判断依据。
功能码有很多,但是最主要用到的是03,06,16,而且软件Modbus Poll也主要是使用这几个功能码,掌握这三个功能码,其余码的使用也是大同小异。
接着我们理解一下这三个功能码的区别:功能码03是查询寄存器,功能码06修改单个寄存器,功能码16是修改连续的多个寄存器。
下面直接实战理解一下。
①功能码03实战:
主机发送: 01 03 00 00 00 01 84 0A
从机回复: 01 03 02 01 02 35 15
解析:
/*主机发送解析*/
01-地址
03-功能码,代表查询功能,其他功能后面再说
00 00-代表查询的起始寄存器地址.说明从0x0000开始查询.
(这里需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据;)
00 01-代表查询了一个寄存器.结合前面的00 00,意思就是查询从0开始的1个寄存器值;
84 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到84前面为止;
(这里新手可能不懂,这个校验就是保证数据传输过程没有错误的一种手段,不同的协议这种校验公式不一样,只需了解这个就足够了,具体怎么求的,可以直接在输出数据得到结果,地址为:http://www.ip33.com/crc.html)
/*从机回复解析*/
01-地址
03-功能码
02-代表后面数据的字节数,因为上面说到,一个寄存器有2个字节,所以后面的字节数肯定是2*查询的寄存器个数;
01 02-寄存器的值是0x0102,结合发送的数据看出,01这个寄存器的值为0x0102
35 15-循环冗余校验
②功能码06实战:
主机发送: 01 06 00 00 12 34 84 BD
从机回复: 01 06 00 00 12 34 84 BD
解析:
/* 主机发送解析 */
01-主机要查的地址
06-功能码,代表修改单个寄存器功能
00 00-代表修改的起始寄存器地址.说明从0x0000开始.
12 34-代表修改的值为0x12 34.结合前面的00 00,意思就是修改0号寄存器值为0x1234;
84 BD -循环冗余校验,是Modbus的校验公式,从首个字节开始到84前面为止;
/*从机回复解析*/
01-从机返回的地址,说明这就是主机查的从机
06-功能码,代表修改单个寄存器功能;
00 00-代表修改的起始寄存器地址.说明是0x0000.
12 34-代表修改的值为0x1234.结合前面的00 00,意思就是修改0号寄存器值为0x1234;
84 BD -循环冗余校验,是Modbus的校验公式,从首个字节开始到84前面为止;
③功能码16实战:
主机发送: 01 10 00 00 00 02 04 00 11 22 33 FB 1F
从机回复: 01 10 00 00 00 02 41 C8
解析:
/*主机发送解析*/
01-主机要查的地址
10-功能码,0x10=16D代表修改多个寄存器功能;
00 00-代表修改的起始寄存器地址.说明从0x0000开始.
00 02-代表修改的寄存器数量,这里开始于0x06的修改不同;
04 -表示修改的总字节数,由于修改了2个寄存器,所以数据要有4个字节;
00 11-表示修改的值,结合上面,就是从第0000寄存器开始修改第一个寄存器值为0x0011,就是把0000寄存器改为0x0011;
22 33-表示修改的值,结合上面,就是从第0000寄存器开始修改第二个寄存器值为0x22 33,就是把0001寄存器改为0x2233;
FB 1F -循环冗余校验,是Modbus的校验公式,从首个字节开始到22前面为止;
/*从机回复解析*/
01-从机返回的地址,说明这就是主机查的从机
10-功能码
00 00-代表修改的起始寄存器地址.说明是0x0000.
00 02-代表修改的寄存器数量,只需要回复这么多久足够了,从机告诉主机,你修改了哪几个寄存器就足够了;
41 C8-循环冗余校验;
五、完成Modbus输入输出代码
我们使用正点原子的战舰开发板,程序的模板基于正点原子的“实验8 定时器中断实验(HAL库)”进行开发,以串口(USART1)作为介质与上位机Modbus Poll通讯。
先说一下输出部分的程序,这个就最简单的,直接调用HAL库的函数就可以了。代码如下:
/* 发送数据函数,buff为发送内容,len表示发送字节数 */
void modbus_send_data(u8 *buff,u8 len)
{
HAL_UART_Transmit(&UART1_Handler,(uint8_t*)buff,len,1000); //发送数据
while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);//等待发送结束
}
然后输入部分,不知道读者记不得前文说的“两帧之间要大于3.5个字节时间间隔,作为一帧结束的判断依据”这句话,那么字节时间怎么计算呢?如果波特率为9600,代表1秒可以传送9600 bits的数据,以业内常用的格式是8位数据,无校验位,1位停止位,加上一个必须的1 bit的起始位,那么一字节的串口数据一共是10位。所以每个字节时间为1/(9600/10)秒,也就是1.04ms。两帧之间要大于3.5个字节时间间隔,那么间隔时间设置为4ms即可。
在程序中,我们定义一个变量modbus_time作为间隔时间的标志位,把modbus_time放入定时器中断服务函数(周期:1ms)中,让其一直增加数值,在串口中断函数中只要收到1Byte消息便会把modbus_time清零,当modbus_time大于4(字节间隔了4ms)且串口曾收到数据但没有接收到新的字节便认为一个帧结构数据接收完成。
举个栗子,有01 02 03数据要发送过来,当数据01发送过来时,串口中断清零modbus_time,此时modbus_time=0,然后过了1.04秒后,数据02发了过来,此时modbus_time=1,但是串口中断清零modbus_time,此时modbus_time又变成0,然后过了1.04秒后,数据03发了过来,此时modbus_time=1,但是串口中断清零modbus_time,此时modbus_time又变成0,最后没有数据再发送过来了,此时modbus_time大于4(字节间隔了4ms)且串口曾收到数据但没有接收到新的字节便认为一个帧结构数据接收完成。
此外我们需要修改串口中断服务函数,把接收状态标记USART_RX_STA的接收完成标志bit15相关功能屏蔽(因为正点原子的原本例程是串口收到0x0d 与 0x0a表示收到完整的串口数据),然后将接收状态标记USART_RX_STA的接收完成标志bit15写在定时器中断函数中作为Modbus接收完成标志位。代码如下
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)//如果是串口1
{
if((USART_RX_STA&0x8000)==0)//接收未完成
{
// if(USART_RX_STA&0x4000)//接收到了0x0d
// {
// if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始
// else USART_RX_STA|=0x8000; //接收完成了
// }
// else //还没收到0X0D
// {
// if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000;
// else
// {
USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ;
USART_RX_STA++;
modbus_time = 0;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
// }
// }
}
}
}
//回调函数,定时器中断服务函数调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==(&TIM3_Handler))
{
modbus_time++;
if((modbus_time>4) &&((modbus_time & 0x3fff)!=0))
{
USART_RX_STA|=0x8000;
}
}
}
六、完成Modbus逻辑功能
这一部分考验C语言的功底,只要清楚Modbus-RTU协议并且C语言功底扎实,相信读者自己也可以办到。详细部分看笔者的源码实现。
七、测试与验证
打开上位机Modbus Poll,然后按下图设置便将上位机连接到板卡。
连接成功后,读者会发现10个寄存器的值会发生改变。这十个数值是程序初设设定的寄存器数值。如下图。
然后我们双击上图数值,便可进入快捷修改寄存器数值界面。然后便可以对功能码06和功能码16进行测试,测试结果如下:
另外点击右侧的“TC”可以自定义写入数据,我们将教程的数据写入测试可以得到下图结果。而且点击类似查找的按钮可以查看当前发送消息和接收消息内容(可以结合快捷修改寄存器数值界面使用)。