学习了很短的时间(three days),但我从代码的阅读中得到了这些。
后来又修订过,并通读了代码, 就不只是3天了!
RTU: 是Remote Terminal Unit的缩写
目标: 基于rs485的modbus 协议。
通讯协议是通讯的两端共同遵守的一些约定。
参考代码:libmodbus(从github下载)
----------------------------------------
协议分析
----------------------------------------
1. 应用数据单元:
ADU:aplication data unit.
先看一个简单实例:
11 05 00 13 ff 00 7f 6f
这一帧数据叫一个ADU:aplication data unit.
它的构成为子机号+PDU+checksum.
modbus 是主从型多机通讯协议,只有主机发起呼叫,从机才能应答。
所以要加加子机号--slaveID
checksum保证数据帧的正确性。
处于等待状态的为 server , 主动发起呼叫的为client,
客户端构建请求信息是indication, 等待服务器回应confirmation
服务器端等待请求信息是indication, 发回confirmation
客户端的任务是 1:组包构建请求(indication),2:发送, 3:接受响应数据(confirmation)
服务器的任务是 1:接受请求(indication), 2:组包构建响应, 3:发回响应(confirmation)
2. 协议数据单元:
PDU:protocal data unit
看上例: 05 00 13 ff 00
由功能号+meta data + extra data 构成, meta data一般是地址2byte+长度2bytes或直接待写的数据,
读功能没有extra data. 写多个数据需要带extra data
3. 功能号
功能号分为以下11种
1 :读线圈
2 :读分散量
3 :读保持寄存器
4 :读输入寄存器
5 :写单个线圈
6 :写单个寄存器
7 :读异常状态
F :写多个线圈
0x10: 写多个寄存器
0x11: 汇报slaveID
0x17: 读和写寄存器
4. meta data:
请求:(request, indication)
读线圈,读分散量,读保持寄存器,读输入寄存器,写单个线圈,写一个寄存器,meta 均为4.
写多个线圈,写多个寄存器,meta 为5
同时读写寄存器,meta为9
其他,读slaveID, 读异常状态,meta 为0
响应:(response, confirmation)
写单个线圈,写单个寄存器,写多个线圈,写多个寄存器,meta 长度为4.
其它,meta 长度为1
问题: 请求何以对写多个线圈,写多个寄存器meta 为5就够了?
答: 因为前4个字节2bytes为地址,2bytes为目标多义长度 ,第5个字节是extra长度字节,后面跟extra data
----------------------------------------
具体的命令,实例解释:
----------------------------------------
1: 读线圈, 功能号 01
请求:11 01 00 13 00 01 0e 9f
11(分机地址,自报家门), 01(功能号)
00 13 00 01(meta 数据, 哪个地址,多少个bit), 0e 9f(校验和)
其中:
00 13 为寄存器地址, 00 01 为一个bit
响应:11 01 01 01 94 88
01. 数据长度,01,具体数值
2: 读分散量, 功能号 02
请求:11 02 00 c4 00 16 ba a9
00 c4 为地址, 00 16 (Nb, number of bits)
响应:11 02 03 ac db 35 20 78
03. 数据长度, ac db 35 为数据
3: 读保持寄存器,功能号 03
请求:11 03 00 66 00 01 f7 46
00 66(地址), 00 01(长度words)
响应:11 03 02 12 34 74 f0
02(长度), 12 34(数据)
4: 读输入寄存器,功能号 04
请求:11 04 00 08 00 01 02 98
响应:11 04 02 00 0a f8 f4.
同5.3, 区别是输入寄存器是不可写的。
5: 写单个线圈,功能号 05
请求:11 05 00 13 ff 00 7f 6f
响应:11 05 00 13 ff 00 7f 6f
响应为copy 回, 00 13(地址) ff 00 (数据),只能是ff 00 或 00 00,
置位1或0
6: 写单个寄存器,功能号 06
请求:11 06 00 66 12 34 f7 f1
响应11 06 00 6b 12 34 f7 f1
响应为copy 回, 00 66(地址) 12 34 (数据)
写一个线圈,写一个寄存器其地址后面的数据都是按16bits 计算的.
有人只实现了03, 06 命令来简化modbus 协议,一定程度上也可以满足要求。
可以看出03, 06 命令是非常重要的,也是最简单的。
7: 读异常状态(忽略)
8: 写多个线圈, 功能号 0xf
11 0f 00 13 00 25 05 cd 6b b2 0e 1b 10 35
00 13(addr), 00 25(len bit), 05(bytes data)
11 0f 00 13 00 25 0e 84
写多个线圈可以用读线圈命令验证。读线圈即可读一个,也可读多个线圈
例如:
11 01 00 13 00 25 0e 84
11 05 05 cd 6b b2 0e 16 45 e6
9: 写多个寄存器 功能号 0x10
11 10 00 6b 00 03 06 02 2b 00 01 00 64 df 84
11 10 00 6b 00 03 f3 44
00 6b(addr), 00 03(len word), 06(bytes), 02 2b 00 01 00 64(6 bytes data)
读保持寄存器也可以读多个寄存器
例如:
11 03 00 6b 00 03 76 87
11 03 06 02 2b 00 01 00 64 99 7a
如果失败,将功能号与0x80相或
11 83 03 00 f4
10: 汇报slaveID (忽略)
11: 读和写寄存器, 功能号 0x17
11 17 00 6b 00 03 00 6c 00 02 04 00 00 00 00 e5 af
00 6b(read addr), 00 03(len word) 00 6c(write addr) 00 02(len word) 04(bytes)
11 17 06 2b 00 00 00 00 09 ae
06(bytes)
----------------------------------------
白话简要概括程序流程:
----------------------------------------
客户端: 以读寄存器为例, 功能号03,需要提供从哪个地址读,读多少bytes, 返回结果放到哪里.
开始
1. 发送请求
1.0 ->建立基本请求包, nb 是按word长度请求的
(分机地址(1)+功能号(1)+地址(2)+nb(2)
1.1 ->计算checksum补充到最后
(分机地址(1)+功能号(1)+地址(2)+nb(2) +checksum(2)
1.2 发送包
2. 接受数据
2.1 接受数据包
2.1.1 先接收2bytes数据,设定超时,用select 函数. 读取数据,
2.1.2 根据功能号可以知道meta数据长度,长度不为0,设定超时,继续接受数据 接受到meta数据,
2.1.3 计算meta数据后面extra数据的长度,长度不为0,设定超时,继续接受数据 接受到extra数据
数据已经全部接受,检查数据完整性
3 检查数据完整性
3.1 slaveID 必须要相等
3.2 计算的checksum必须要与发送的checksum相等
3.3 检查请求的ID(req[0])和响应的ID(rsp[0])是否相等,不等是否是广播地址(0),否则出错
3.4 从请求包中计算响应长度,与接受包比较.
3.5 比较响应的功能码与发送的功能码是否相同
3.6 比较请求的长度和响应的长度是否一致
4 将接受数据copy 到输出(由于目标是16bits,所以需要高低字节(2个byte)的拼凑到输出)
(分机地址(1)+功能号(1)+长度(按byte计算)+extra(数据)+checksum(2)
其它功能流程类似,每一个功能都会有一个流程函数, 在细节上会有所不同,例如读线圈bit, 最后拼凑的是bit 到byte.
服务器程序就不详细描述了,只写大概流程.
1. 接受请求包(slaveid(1)+func(1)+meta+extra, 并校验完整性(checksum)
2. 构造confirmation包(slaveid(1)+func(1)+len(1)+extra,并发回响应.
----------------------------------------
modbus 库的调用流程
跟白话调用一致,但落实到具体的库函数上.
----------------------------------------
服务器端:
--------------------
甲: 初始化ctx
ctx = modbus_new_rtu("/dev/ttyUSB1", 9600, 'N', 8, 1);
modbus_set_slave(ctx, 1);
modbus_connect(ctx);
1. ctx = modbus_new_rtu("/dev/ttyUSB1", 9600, 'N', 8, 1);
创建rtu上下文, 实际上是分配内存保存信息和设定后端函数指针等数据结构.
2. static int _modbus_set_slave(modbus_t *ctx, int slave)
将slave ID 设定为从模式的ID, 或者在主模式下,设定将要与之对话的远程终端的ID
3. connect 函数, 何时完成连接的建立
看代码知道,connect 只是打开和设置了串口设备,并没有发起连接请求。
乙: 分配共享的数据
mb_mapping = modbus_mapping_new(MODBUS_MAX_READ_BITS, 0,
MODBUS_MAX_READ_REGISTERS, 0);
设定共享的数据大小,开始地址为0.
丙: 接受请求和应答.
for(;;) {
uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH];
rc = modbus_receive(ctx, query);
modbus_reply(ctx, query, rc, mb_mapping);
}
1. rc = modbus_receive(ctx, query);
从远端接受到数据,存入query, 当然应该对rc 返回值进行判别
2. modbus_reply(ctx, query, rc, mb_mapping);
将mb_mapping 中数据根据query 回应过去
--------------------
客户端
--------------------
甲: 初始化ctx,
打开端口,准备通讯.
请参考服务器端描述.
乙: 发起请求, 例如03指令,读取寄存器
rc = modbus_read_registers(ctx, 0, nb_points, tab_reg);
该命令表示请求读取从0地址开始的nb_points 个寄存器, 返回值在tab_reg 中
其它指令此处忽略
看头文件,读其代码,可以领会其实质。代码值得一读。
概论modbus 协议,是一种数据传输协议, 着重解决你是谁(who,slaveID),你想干什么(what,funcNo),
数据在哪里(where,addr:length)的问题,完成数据交互.
对于特定应用,实现03,06功能就足够了,有的公司就是这么用的.