文章目录
1. 起因
最近在做一个项目,我们的设备使用485作为从机与主机PLC通信,用到了modbus,因此整理了一份modbus的资料。
其实modbus还是很简单的,虽然有些名词初看不知道是啥,看懂之后发现就是绕人用的,换个名词就很清晰易懂的(对于我这种不做PLC的人)。
2. 几个重点
2.1 一些难懂的概念
- 线圈 就是“位变量”
- 离散输入 就是 只读 的 1位变量
- 线圈 就是 可读写 的 1位变量
- 寄存器就是“16位变量”
- 输入寄存器 就是 只读 的 16位变量
- 保持寄存器 就是 可读写 的 16位变量
具体可以见5章节,阅读的时候把这几个名词直接替换就好。
2.2 CRC的高低位
Modbus协议的CRC校验码是高位在前还是低位在前的问题。
有些资料说高位在前,有些资料是说低位在前。
其实是因为CRC生成函数,有些在内部已经做了高低位的转换,所以看起来像是高位在前。
实际上,CRC是低位在前发送。详细见第7节。
2.3 其他
- 不管任何时候,从机都不能主动向主机发送数据
- 从站的地址是1~247(注意没有0!! 0是广播!都不回复,都会收到!)
- 数字编码大端序,高字节在前
- CRC校验位低字节在前
- modbus的寄存器都是16位的,但是在其他地方(比如MCGS)也见过一些变种,比如32位的,比如float的。有些是把32位按照标准的modbus,拆分成了2个寄存器。有些就是按1个4字节的寄存器来算的。
3. 介绍
3.1 起源
modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。
1996年施耐德公司推出基于以太网TCP/IP的modbus协议:modbusTCP。
3.2 分类
Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。ASCII是明文的,但是实际效率低,用的比较普遍的就是RTU格式。
这里我也是只用了RTU,就只记录RTU的协议。
4. 格式
4.1 串口协议
- 串口波特率
- 串口设置
4.2 帧格式
起始位 | 地址码 | 功能码 | 数据区 | 错误校验码 |
---|---|---|---|---|
3.5字符时间 | 1字节 | 1字节 | 0~252字节 | CRC16校验码 |
1~247 | 见3.5节 | CRC低位 CRC高位 | ||
注意这里,多字节的数据,是高位在前,低位在后。 | ||||
但是CRC是低位在前,高位在后。 | ||||
根据不同的功能码,数据区各不相同,具体可以看第8部分实例来分析。 |
5. 数据类型
数据类型 | 简写 | 读功能码 | 写功能码 | 对象 | 地址范围 |
---|---|---|---|---|---|
离散输入 | DI | 02 | 只读的 1位变量 | 00001~0FFFF | |
线圈 | DO | 01 | 05,15 | 可读写的 1位变量 | 10001~1FFFF |
输入寄存器 | AI | 04 | 只读的 16位变量 | 30001~3FFFF | |
保存寄存器 | AO | 03 | 06,16 | 可读写的 16位变量 | 40001~4FFFF |
- DI: 数字输入,离散输入,一个地址一个数据位,用户只能读取它的状态,不能修改。比如面板上的按键、开关状态,电机的故障状态。
- DO: 数字输出,线圈输出,一个地址一个数据位,用户可以置位、复位,可以回读状态,比如继电器输出,电机的启停控制信号。
- AI: 模拟输入,输入寄存器,一个地址16位数据,用户只能读,不能修改,比如一个电压值的读数。
- AO: 模拟输出,保持寄存器,一个地址16位数据,用户可以写,也可以回读,比如一个控制变频器的电流值。
6. 功能码
功能码很多,用的比较多的就是以下8个:
功能码 | 功能码16进制 | 中文名称 | 操作位 | 操作寄存器数量 |
---|---|---|---|---|
01 | 0x1 | 读线圈 | 1位 | 单个或多个 |
02 | 0x2 | 读离散输入 | 1位 | 单个或多个 |
03 | 0x3 | 读保存寄存器 | 16位 | 单个或多个 |
04 | 0x4 | 读输入寄存器 | 16位 | 单个或多个 |
05 | 0x5 | 写单个线圈 | 1位 | 单个 |
06 | 0x6 | 写单个保存寄存器 | 16位 | 单个 |
15 | 0xF | 写多个线圈 | 1位 | 多个 |
16 | 0x10 | 写多个保持寄存器 | 16位 | 多个 |
最主要的就是1、2、3、4、5、6、15、16 这8个功能码。
个人翻译一下
01:读一个或多个可读写的 bit 变量
02:读一个或多个只能读的 bit 变量
03:读一个或多个可读写的 short 变量 (或其他2字节变量)
04:读一个或多个只能读的 short 变量 (或其他2字节变量)
05:写一个可读写的 bit 变量
06:写一个可读写的 short 变量
15:写多个可读写的 bit 变量
16:写多个可读写的 short变量 (可以拼凑成int 、long 、long long、float等变量)
读的时候,都是可以选择读“一个或多个”。
写的时候,按数量分功能码进行操作。
7. CRC16(modbus)
在CRC计算时只用8个数据位,起始位及停止位,如有奇偶校验位也包括奇偶校验位,都不参与CRC计算。
CRC计算方法是:
- 加载一值为0XFFFF的16位寄存器,此寄存器为CRC寄存器。
- 把第一个8位二进制数据(即通讯信息帧的第一个字节)与16位的CRC寄存器的相异或,异或的结果仍存放于该CRC寄存器中。
- 把CRC寄存器的内容右移一位,用0填补最高位,并检测移出位是0还是1。
- 如果移出位为零,则重复第三步(再次右移一位);如果移出位为1,CRC寄存器与0XA001进行异或。
- 重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理。
- 重复步骤2和5,进行通讯信息帧下一个字节的处理。
- 将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换
- 最后得到的CRC寄存器内容即为:CRC校验码。
注意!这个 高低字节交换 之后的,才是算出来的modbus的CRC校验码。
然后把这个交换后的校验码,按照 先低位,后高位 的 顺序加载到发送队列里。
8. 实例
从网上随便截取了一些别人的例子,部分来自示例
8.1 功能码1
8.2 功能码2
8.3 功能码3
8.4 功能码4
主机要读取从0x400地址的2个16位寄存器的值。
从机回复了4个字节。
8.5 功能码5
8.6 功能码6
8.7 功能码15
8.8 功能码16
9. 实现思路
9.1 我的实现思路
我这边实现思路比较简单,
利用串口中断判断收到的报文,如果满足报头(设备地址),就进行报文连续接收,收满一帧就放入处理队列。另一个线程收到这个数据会取出来解包处理。
寄存器方面就是申请一块连续buf(数组或其结构体),和寄存器地址一一对应上,要改值或者要读值都是根据这个buf的数据来做。
9.2 别人的实现思路(一些开源库)
用的比较广的是FreeModbus和LibModbus。