以“modbus_write_bits”函数为例,分析下图的执行流程:
11.3.1
初始化
1. 主设备初始化
主设备程序先调用“modbus_new_rtu”函数,仅仅是分配一个modbus结构体,在里面记录要使用的串口设备、参数:
modbus_t *
modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bi
t);
再调用“modbus_set_slave”,是设置“要访问哪个从设备”。每当访问不同地址的设备之前,都应该调用这个函数。
最后调用“modbus_connect”函数,这个函数只是打开串口、设置串口参数,并没有跟从设备进行数据交互。
2. 从设备初始化
从设备的初始化,跟主设备类似,不过多了使用“modbus_mapping_new_start_address”函数创建寄存器 buffer。
modbus_mapping_t结构体如下定义:
typedef struct _modbus_mapping_t {
int nb_bits;
int start_bits;
int nb_input_bits;
int start_input_bits;
int nb_input_registers;
int start_input_registers;
int nb_registers;
int start_registers;
uint8_t *tab_bits;
uint8_t *tab_input_bits;
uint16_t *tab_input_registers;
uint16_t *tab_registers;
} modbus_mapping_t;
它被用来描述DI、DO、AI、AO四类寄存器。以DO寄存器为例,这个结构体里有3个成员:
①nb_bits:DO寄存器个数。
②start_bits:DO寄存器起始寄存器地址。
③tab_bits:指向一个“uint8_t”类型的数组,里面每个数组项表示一个DO寄存器,这个数组大小为nb_bits。数组中第0项,对应第“start_bits”个DO寄存器。
“modbus_mapping_new_start_address”函数原型
如下:
/* Allocates 4 arrays to store bits, input bits, registers and inputs
registers. The pointers are stored in modbus_mapping structure.
The modbus_mapping_new_start_address() function shall return the new allocated
structure if successful. Otherwise it shall return NULL and set errno to
ENOMEM. */
modbus_mapping_t *modbus_mapping_new_start_address(unsigned int start_bits,
unsigned int nb_bits,
unsigned int start_input_bits,
unsigned int nb_input_bits,
unsigned int start_registers,
unsigned int nb_registers,
unsigned int start_input_registers,
unsigned int nb_input_registers);
假设从设备执行了如下代码:
modbus_mapping_t *mb_mapping;
mb_mapping = modbus_mapping_new_start_address(0,
4, /* DO, 4 个寄存器 */
0,
3, /* DI, 3 个寄存器 */
0,
2, /* AO, 2 个寄存器 */
0,
1; /* AI, 1 个寄存器 */
将会分配出如下结构体:
modbus传输的本质,就是读写上图中4个数组。
11.3.2
主设备发送请求
主设备调用“modbus_write_bits”函数,想写若干个DO寄存器,比如:
01 uint8_t buf[2] = {1, 0};
02 int rc = modbus_write_bits(ctx, 0, 2, buf);
根据Modbus RTU协议,它必定执行如下操作:
①构造数据包
②通过串口发送数据包
③等待、读取回复
对于上述代码,数据包的内容如下:
1. 先构造包头
函数调用关系如下:
2. 再构造数据
代码如下:
3. 计算检验码
在发送数据包的函数里,先计算检验码,代码如下:
4. 发送数据包
前面构造好了数据包,发送就比较简单:调用write函数进行发送即可。代码如下:
11.3.3
从设备接收请求
从设备的程序一直在等待主机发来的消息,示例代码如下:
“modbus_receive”函数内部实现为:
①使用select机制,逐个读取字符
②根据读到的字符,分辨还需要读多少数据
1. 读取单个字符
函数调用关系如下:
2. 判断还需要读取多少数据
从设备读取主设备发来的请求包时,步骤为:
①先读取“功能码”
②再根据功能码判断后续要的包头数据还剩多少,读取包头
③最后根据包头数据解析要读多少数据,读取数据。
流程如下图所示:
确定第1个阶段数据长度的代码如下:
读到功能码后,根据功能码计算剩下的包头的数据:
读到完整的包头后,计算剩下的数据长度:
3. 判断数据完整性
就是根据校验码判断数据是否有错误,代码如下:
11.3.4
从设备回应
从设备接收到请求后,调用如下函数进行处理、回应:
在“modbus_reply”函数内部,它会:
①对于写请求:把请求包中的数据解析出入,填入 mb_mapping中对应的寄存器buffer;
②对于读请求:从mb_mapping中对应的寄存器buffer取出数据;
③构造回复包,发送给主设备。
本情景分析中,主设备调用“modbus_write_bits”函数,想写两个DO寄存器,比如:
01 uint8_t buf[2] = {1, 0};
02 int rc = modbus_write_bits(ctx, 0, 2, buf);
从设备使用“modbus_reply”函数处理。
1. 根据请求包设置寄存器buffer
代码如下:
2. 构造回复包
对于“写多个DO寄存器”的请求,它的回复包格式如下:
下面的代码,构造的回复包里含有上图1、2的信息(在发送回复包时才构造校验码):
3. 发送回复包
最后,从设备发送回复包:
如您在使用瑞萨MCU/MPU产品中有任何问题,可识别下方二维码或复制网址到浏览器中打开,进入瑞萨技术论坛寻找答案或获取在线技术支持。
https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/
未完待续
推荐阅读
写多个线圈与写多个保持寄存器 - RZ MPU工业控制教程连载(35)
libmodbus开发库 - RZ MPU工业控制教程连载(37)