unit-test-server.c
我们接着从源码的单元测试服务器(从机)libmodbus-3.1.10\tests\unit-test-server.c
的 main
函数开始分析源码执行流和所用的数据结构。推进学习百问网提供的全场景工业互联设备管理系统解决方案,其中有对 libmodbus 源码详细的视频课程讲解。
可以看到对于从机,初始化时的操作是类似的
use_backend = RTU; /* 设置传输模式为 RTU */
ip_or_device = "/dev/ttyUSB0"; /* 设置串口设备 */
ctx = modbus_new_rtu(ip_or_device, 115200, 'N', 8, 1);
modbus_set_slave(ctx, SERVER_ID); /* 从机设置自己的地址 */
query = malloc(MODBUS_RTU_MAX_ADU_LENGTH); /* 分配一个 ADU 最大的内存空间 */
/* Modbus_Application_Protocol_V1_1b.pdf Chapter 4 Section 1 Page 5
* RS232 / RS485 ADU = 253 bytes + slave (1 byte) + CRC (2 bytes) = 256 bytes
*/
#define MODBUS_RTU_MAX_ADU_LENGTH 256
构建服务器(从机)端的 Modbus 数据模型内存映射
/* 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_new_start_address()
函数它分配内存来映射存储 Modbus 中的数据模型(线圈,输入离散值,保持寄存器和输入寄存器)的数据结构。这些结构在 Modbus 通信过程中用于管理和映射数据。如下图所示
该函数创建并初始化一个 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;
该函数它包含了以下映射:
- 位 (0X):用于读取和写入输出的线圈状态。
- 输入位 (1X):用于读取输入离散状态。
- 寄存器 (4X):用于读取和写入数据的保持寄存器。
- 输入寄存器 (3X):用于读取输入寄存器的值。
该函数它接受以下参数来配置每个映射的内存分配:
- start_bits:线圈的起始地址。
- nb_bits:线圈的数量。
- start_input_bits:输入离散值的起始地址。
- nb_input_bits:输入离散值的数量。
- start_registers:保持寄存器的起始地址。
- nb_registers:保持寄存器的数量。
- start_input_registers:输入寄存器的起始地址。
- nb_input_registers:输入寄存器的数量。
该函数使用动态内存分配来创建这些映射,并返回指向 modbus_mapping_t
结构体的指针。
/* 首先为 modbus_mapping_t 结构体分配内存。*/
mb_mapping = (modbus_mapping_t *) malloc(sizeof(modbus_mapping_t));
接着为各种数据模型分配内存。
/* 0X (线圈) */
mb_mapping->nb_bits = nb_bits;
mb_mapping->start_bits = start_bits;
if (nb_bits == 0) {
mb_mapping->tab_bits = NULL; // 如果没有线圈需要映射,则不分配内存。
} else {
// 为线圈分配内存。
mb_mapping->tab_bits = (uint8_t *) malloc(nb_bits * sizeof(uint8_t));
if (mb_mapping->tab_bits == NULL) {
free(mb_mapping); // 如果内存分配失败,释放已分配的内存。
return NULL;
}
memset(mb_mapping->tab_bits, 0, nb_bits * sizeof(uint8_t)); // 初始化为 0。
}
/* 1X (输入离散值) */
mb_mapping->nb_input_bits = nb_input_bits;
mb_mapping->start_input_bits = start_input_bits;
if (nb_input_bits == 0) {
mb_mapping->tab_input_bits = NULL; // 如果没有输入离散值需要映射,则不分配内存。
} else {
// 为输入离散值分配内存。
mb_mapping->tab_input_bits = (uint8_t *) malloc(nb_input_bits * sizeof(uint8_t));
if (mb_mapping->tab_input_bits == NULL) {
free(mb_mapping->tab_bits); // 释放已分配的内存。
free(mb_mapping);
return NULL;
}
memset(mb_mapping->tab_input_bits, 0, nb_input_bits * sizeof(uint8_t)); // 初始化为 0。
}
/* 4X (保持寄存器) */
mb_mapping->nb_registers = nb_registers;
mb_mapping->start_registers = start_registers;
if (nb_registers == 0) {
mb_mapping->tab_registers = NULL; // 如果没有保持寄存器需要映射,则不分配内存。
} else {
// 为保持寄存器分配内存。
mb_mapping->tab_registers = (uint16_t *) malloc(nb_registers * sizeof(uint16_t));
if (mb_mapping->tab_registers == NULL) {
free(mb_mapping->tab_input_bits); // 释放已分配的内存。
free(mb_mapping->tab_bits);
free(mb_mapping);
return NULL;
}
memset(mb_mapping->tab_registers, 0, nb_registers * sizeof(uint16_t)); // 初始化为 0。
}
/* 3X (输入寄存器) */
mb_mapping->nb_input_registers = nb_input_registers;
mb_mapping->start_input_registers = start_input_registers;
if (nb_input_registers == 0) {
mb_mapping->tab_input_registers = NULL; // 如果没有输入寄存器需要映射,则不分配内存。
} else {
// 为输入寄存器分配内存。
mb_mapping->tab_input_registers =
(uint16_t *) malloc(nb_input_registers * sizeof(uint16_t));
if (mb_mapping->tab_input_registers == NULL) {
free(mb_mapping->tab_registers); // 释放已分配的内存。
free(mb_mapping->tab_input_bits);
free(mb_mapping->tab_bits);
free(mb_mapping);
return NULL;
}
memset(mb_mapping->tab_input_registers, 0, nb_input_registers * sizeof(uint16_t)); // 初始化为 0。
}
```c
---
```c
return mb_mapping; // 返回指向分配的 modbus_mapping_t 结构体的指针。
建立连接
rc = modbus_connect(ctx);
if (rc == -1) {
fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
_modbus_rtu_receive
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
这个函数的用来接收 Modbus RTU 消息,并根据协议的需要进行处理。
static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req)
{
int rc;
modbus_rtu_t *ctx_rtu = ctx->backend_data;
/* 检查是否设置了忽略下一个确认消息的标志 */
if (ctx_rtu->confirmation_to_ignore) {
/* 如果设置了忽略标志,接收消息作为确认(不处理)*/
_modbus_receive_msg(ctx, req, MSG_CONFIRMATION);
/* 在接收确认消息后,重置该标志 */
ctx_rtu->confirmation_to_ignore = FALSE;
rc = 0; /* 设置返回代码为 0,表示成功处理(没有错误)*/
/* 如果启用了调试模式,打印提示消息,表明确认消息被忽略 */
if (ctx->debug) {
printf("Confirmation to ignore\n");
}
} else {
/* 如果没有设置忽略标志,继续接收常规消息(指示消息)*/
rc = _modbus_receive_msg(ctx, req, MSG_INDICATION);
/* 如果没有发生错误(rc == 0),则设置标志,表明下一个消息应该是需要忽略的确认消息 */
if (rc == 0) {
ctx_rtu->confirmation_to_ignore = TRUE;
}
}
/* 返回接收操作的结果代码 */
return rc;
}
在前面执行的 modbus_new_rtu
函数时,函数内部 modbus-rtu.c
中第 #1285
行初始化 ctx_rtu->confirmation_to_ignore = FALSE
服务器端(从机)在开始建立连接并接收请求时会直接执行 _modbus_receive_msg
函数。
简单说下 _modbus_receive_msg
函数内部执行流程。
while (length_to_read != 0) {
// 1. 使用 select 函数等待数据可读
rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);
// ... (此处错误处理)
// 2. 接收数据并将其存储在消息缓冲区中
rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);
// ... (此处错误处理)
// 3. 如果调试模式开启,打印接收到的每个字符的十六进制值
if (ctx->debug) {
int i;
for (i = 0; i < rc; i++)
printf("<%.2X>", msg[msg_length + i]);
}
// 4. 更新已接收的字节数
msg_length += rc;
// 5. 计算剩余的待读取字节数
length_to_read -= rc;
// 6. 如果当前步骤的字节已经全部读取完毕,进入下一步骤
if (length_to_read == 0) {
switch (step) {
case _STEP_FUNCTION:
// 6.1 计算功能码后的元数据长度
length_to_read = compute_meta_length_after_function(
msg[ctx->backend->header_length], msg_type);
if (length_to_read != 0) {
step = _STEP_META; // 进入元数据读取步骤
break;
} // 如果元数据长度为0,直接进入下一步
case _STEP_META:
// 6.2 计算元数据后的数据长度
length_to_read = compute_data_length_after_meta(ctx, msg, msg_type);
if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
// 如果数据长度超过最大允许长度,返回错误
errno = EMBBADDATA;
_error_print(ctx, "too many data");
return -1;
}
step = _STEP_DATA; // 进入数据读取步骤
break;
default:
break;
}
}
// 7. 如果还有待读取的字节,并且设置了字节间超时,则更新超时时间
if (length_to_read > 0 &&
(ctx->byte_timeout.tv_sec > 0 || ctx->byte_timeout.tv_usec > 0)) {
// 7.1 设置字节间超时时间
tv.tv_sec = ctx->byte_timeout.tv_sec;
tv.tv_usec = ctx->byte_timeout.tv_usec;
p_tv = &tv;
}
// 8. 如果没有设置字节间超时,则必须在响应超时之前读取完整响应(仅用于确认)
}
以上代码都是在 Linux 环境中实现的,需要改成我们的裸机版本和FreeRTOS版本。
-
select
函数:- 用于等待数据可读。
select
是一个常见的 I/O 多路复用函数,可以监视多个文件描述符的状态变化。 ctx->backend->select
是一个抽象的后端函数,用于检查是否有数据可读。p_tv
是超时时间,如果在指定时间内没有数据可读,select
会返回。
- 用于等待数据可读。
-
recv
函数:- 用于从连接中接收数据。
ctx->backend->recv
是一个抽象的后端函数,用于接收数据。 - 接收到的数据存储在
msg
缓冲区中,从msg_length
位置开始存储。
- 用于从连接中接收数据。
modbus_send_raw_request
modbus_reply
通过 modbus_send_raw_request
发送原始请求作为回应或者 modbus_reply
给客户端(主机)发送回复。
modbus_mapping_free
modbus_close
modbus_free
通过 modbus_mapping_free
释放数据模型内存
通过 modbus_close
关闭连接
通过 modbus_free
释放上下文对象