1. libmodbus RTU模式初始化与上下文创建详解
在工业控制领域,Modbus协议因其简单可靠而广受欢迎。作为Modbus协议栈中的经典实现,libmodbus库为开发者提供了便捷的API接口。本文将深入剖析libmodbus RTU模式的初始化过程,特别关注在嵌入式系统中的实现细节。
1.1 RTU模式创建函数modbus_new_rtu()
modbus_new_rtu()是使用libmodbus库时的第一个关键函数,它负责创建并初始化RTU模式的Modbus上下文。这个函数的原型如下:
c复制modbus_t *modbus_new_rtu(
const char *device, // 串口设备名(如"/dev/ttyUSB0"或"COM1")
int baud, // 波特率(如9600、115200)
char parity, // 奇偶校验:'N'=无,'E'=偶,'O'=奇
int data_bit, // 数据位(通常为8)
int stop_bit // 停止位(1或2)
);
在嵌入式系统中,设备名参数需要根据具体硬件平台调整。例如在STM32上可能使用"USART1"这样的标识符,而在Linux系统则使用"/dev/ttyS0"这样的设备节点。
1.1.1 内存分配流程
函数内部的内存分配遵循严格的顺序,确保资源正确释放:
c复制// 1. 分配主上下文结构体
ctx = (modbus_t *)malloc(sizeof(modbus_t));
if (ctx == NULL) {
return NULL;
}
// 2. 初始化通用部分
modbus_init_common(ctx);
// 3. 设置后端为RTU模式
ctx->backend = &modbus_rtu_backend;
// 4. 分配RTU专用数据空间
ctx->backend_data = (modbus_rtu_t *)malloc(sizeof(modbus_rtu_t));
if (ctx->backend_data == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
// 5. 获取RTU数据指针(方便访问)
ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
在资源受限的嵌入式系统中,开发者可能需要修改这个内存分配过程,例如使用静态分配代替动态内存分配,或者实现自定义的内存管理策略。
1.1.2 设备名存储的特殊处理
设备名字符串的存储需要考虑跨平台兼容性:
c复制// 分配设备名内存(长度+1是为了存放字符串结束符'\0')
ctx_rtu->device = (char *)malloc((strlen(device) + 1) * sizeof(char));
if (ctx_rtu->device == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
// 复制设备名(跨平台兼容)
#if defined(_WIN32)
strcpy_s(ctx_rtu->device, strlen(device) + 1, device);
#else
strcpy(ctx_rtu->device, device); // 单片机通常用这个
#endif
在嵌入式系统中,如果设备名是固定的,可以考虑直接使用指针引用常量字符串,避免动态内存分配。
1.2 串口参数配置(针对单片机)
1.2.1 基本参数设置
串口通信的基本参数直接影响通信质量,这些参数必须与从站设备严格匹配:
c复制// 设置数据位和停止位(这两项是单片机通信的关键)
ctx_rtu->data_bit = data_bit; // 通常为8
ctx_rtu->stop_bit = stop_bit; // 通常为1
ctx_rtu->parity = parity; // 校验位设置
ctx_rtu->baud = baud; // 波特率
在嵌入式开发中,还需要注意硬件UART外设对这些参数的支持范围。某些低端MCU可能不支持所有波特率或特定的停止位配置。
1.2.2 单字节传输时间计算
RTU模式依赖精确的时间控制,单字节传输时间的计算至关重要:
c复制// 计算传输一个字节需要的时间(微秒)
ctx_rtu->onebyte_time =
1000000 * (1 + data_bit + (parity == 'N' ? 0 : 1) + stop_bit) / baud;
这个计算公式的各个部分解释如下:
1000000:将秒转换为微秒(1秒=1,000,000微秒)1:起始位(总是存在)data_bit:数据位数(parity == 'N' ? 0 : 1):奇偶校验位(有则1,无则0)stop_bit:停止位数baud:波特率(位/秒)
以9600bps、8N1配置为例:
code复制单字节时间 = 1000000 × (1 + 8 + 0 + 1) / 9600
= 1000000 × 10 / 9600
≈ 1041.67微秒
≈ 1.04ms
这个时间值将用于后续的帧间隔检测和超时计算,在嵌入式系统中必须精确计算。
1.2.3 RTS(请求发送)相关设置
对于RS485通信,RTS信号控制发送使能:
c复制// RTS控制模式(单片机中常用)
ctx_rtu->rts = MODBUS_RTU_RTS_NONE; // 默认不控制
// 设置RTS控制函数(单片机可能需要重写这个函数)
ctx_rtu->set_rts = _modbus_rtu_ioctl_rts;
// RTS切换延时(使用单字节传输时间)
ctx_rtu->rts_delay = ctx_rtu->onebyte_time;
在嵌入式系统中,通常需要实现自定义的RTS控制函数,例如:
c复制void mcu_set_rts(modbus_t *ctx, int on) {
if (on) {
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(RS485_RE_GPIO_Port, RS485_RE_Pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(RS485_RE_GPIO_Port, RS485_RE_Pin, GPIO_PIN_RESET);
}
}
1.3 RTU后端函数表(核心操作集)
libmodbus采用面向对象的设计思想,通过函数表实现不同后端的多态:
c复制const modbus_backend_t _modbus_rtu_backend = {
.backend_type = MODBUS_BACKEND_TYPE_RTU,
.header_length = MODBUS_RTU_HEADER_LENGTH, // RTU头长度
.checksum_length = MODBUS_RTU_CHECKSUM_LENGTH, // CRC校验长度
.max_adu_length = MODBUS_RTU_MAX_ADU_LENGTH, // 最大ADU长度
// 关键函数指针
.set_slave = _modbus_set_slave, // 设置从站地址
.build_request_basis = _modbus_rtu_build_request_basis, // 构建请求
.build_response_basis = _modbus_rtu_build_response_basis, // 构建响应
.send_msg_pre = _modbus_rtu_send_msg_pre, // 发送前处理
.send = _modbus_rtu_send, // 发送数据
.receive = _modbus_rtu_receive, // 接收数据
.check_integrity = _modbus_rtu_check_integrity, // CRC校验
.connect = _modbus_rtu_connect, // 连接串口
.close = _modbus_rtu_close, // 关闭串口
.flush = _modbus_rtu_flush, // 清空缓冲区
.select = _modbus_rtu_select, // 多路复用(单片机不常用)
.free = _modbus_rtu_free // 释放资源
};
在嵌入式系统中,可能需要重写部分函数实现,特别是与硬件直接相关的操作如send、receive等。
1.4 RTU协议格式说明
RTU模式的帧格式相对固定:
code复制RTU帧格式:
+--------+--------+--------+----------+------------+--------+--------+
| 地址 | 功能码 | 数据 | ... | 数据 | CRC低 | CRC高 |
+--------+--------+--------+----------+------------+--------+--------+
| 1字节 | 1字节 | n字节 | ... | ... | 1字节 | 1字节 |
+--------+--------+--------+----------+------------+--------+--------+
总长度:1 + 1 + n + 2 = n+4字节
RTU模式的特点包括:
- 二进制数据传输,效率高于ASCII模式
- 依赖精确的时序控制
- 使用CRC校验保证数据完整性
- 帧间隔要求严格(至少3.5个字符时间)
2. 从站地址设置与Modbus上下文结构详解
2.1 Modbus主/从模式基础概念
2.1.1 两种工作模式
Modbus通信基于主从架构:
code复制Modbus通信模式:
├── 主站模式(Master)
│ ├── 发起请求
│ ├── 控制通信时序
│ └── 一个主站可对应多个从站
│
└── 从站模式(Slave)
├── 响应请求
├── 被动接收命令
└── 每个从站有唯一地址(1-247)
在嵌入式系统中,设备通常作为从站运行,但某些网关设备可能需要实现主站功能。
2.1.2 地址范围规则
Modbus地址分配遵循特定规范:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0 | 广播地址 | 所有从站都能接收,但不回复响应 |
| 1-247 | 正常从站地址 | 标准Modbus RTU地址范围 |
| 248-255 | 保留地址 | 特殊用途或扩展地址 |
广播地址(0)的特殊性:
- 只能用于写操作(功能码05、06、15、16等)
- 从站不应回复响应
- 用于同时控制多个从站的场景
2.2 Modbus上下文结构体(核心数据结构)
2.2.1 结构体定义分析
modbus_t结构体封装了Modbus通信的所有状态信息:
c复制struct modbus {
// 1. 从站地址(核心参数)
int slave; // 当前设备的从站地址
// 2. 文件描述符
int s; // Socket或文件描述符(串口/网络)
// 3. 调试和错误处理
int debug; // 调试模式开关
int error_recovery; // 错误恢复模式
int quirks; // 特殊处理标志(兼容性)
// 4. 超时设置(重要!)
struct timeval response_timeout; // 响应超时
struct timeval byte_timeout; // 字节接收超时
struct timeval indication_timeout; // 指示超时(TCP用)
// 5. 后端框架
const modbus_backend_t *backend; // 指向后端函数表
void *backend_data; // 指向RTU/TCP专用数据
};
在嵌入式系统中,s字段的含义可能不同,通常代表UART外设的句柄或标识符。
2.2.2 成员详细解释
slave成员:
c复制int slave; // 取值范围:0-255
- 主站模式时:表示要访问的目标从站地址
- 从站模式时:表示自身的地址
- 特殊值0:广播地址
文件描述符s:
c复制int s; // 在单片机中通常是串口句柄
- 在RTU模式下:串口文件描述符
- 在TCP模式下:socket描述符
- 单片机中:可能是UART句柄编号或寄存器地址
超时设置(关键参数):
c复制struct timeval response_timeout; // 完整响应的超时时间
struct timeval byte_timeout; // 单个字节接收的超时时间
timeval结构体定义:
c复制struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
在嵌入式系统中,超时设置对通信稳定性至关重要,需要根据波特率精心计算。
2.3 设置从站地址函数详解
2.3.1 主函数:modbus_set_slave()
c复制int modbus_set_slave(modbus_t *ctx, int slave)
{
// 1. 参数有效性检查
if (ctx == NULL) {
errno = EINVAL; // 设置错误码:无效参数
return -1;
}
// 2. 调用后端的具体实现函数
return ctx->backend->set_slave(ctx, slave);
}
调用示例:
c复制// 设置从站地址为1
if (use_backend == RTU) {
modbus_set_slave(ctx, SERVER_ID); // SERVER_ID = 1
}
2.3.2 实际实现函数:_modbus_set_slave()
c复制static int _modbus_set_slave(modbus_t *ctx, int slave)
{
// 1. 确定最大从站地址
int max_slave = (ctx->quirks & MODBUS_QUIRK_MAX_SLAVE) ? 255 : 247;
/* quirk解释:
MODBUS_QUIRK_MAX_SLAVE是一个特殊标志位
当设置了该标志时,允许使用扩展地址范围(0-255)
否则使用标准地址范围(0-247)
*/
// 2. 地址范围验证
/* 广播地址是0 (MODBUS_BROADCAST_ADDRESS) */
if (slave >= 0 && slave <= max_slave) {
// 3. 设置地址到上下文
ctx->slave = slave;
} else {
// 4. 地址无效,设置错误
errno = EINVAL; // 错误码:EINVAL = 无效参数
return -1;
}
return 0;
}
2.4 单片机应用注意事项
2.4.1 地址设置时机
正确的使用顺序:
c复制modbus_t *ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
if (ctx == NULL) {
// 处理错误
}
// 设置为主站模式,目标从站地址为1
modbus_set_slave(ctx, 1); // 作为主站,要访问从站1
// 或者设置为从站模式,自身地址为1
modbus_set_slave(ctx, 1); // 作为从站,自身地址为1
2.4.2 广播地址的特殊处理
c复制// 广播地址(地址0)的使用
modbus_set_slave(ctx, MODBUS_BROADCAST_ADDRESS); // 地址0
// 广播特点:
// 1. 所有从站都会接收命令
// 2. 没有从站会回复响应
// 3. 用于同时控制多个从站
// 4. 只能用于写操作(不能读)
2.4.3 超时设置(单片机中重要!)
c复制// 设置超时(针对单片机可能调整)
struct timeval timeout;
timeout.tv_sec = 1; // 1秒
timeout.tv_usec = 0; // 0微秒
// 设置响应超时
modbus_set_response_timeout(ctx, &timeout);
// 设置字节超时(更关键)
timeout.tv_sec = 0;
timeout.tv_usec = 500000; // 0.5秒=500毫秒
modbus_set_byte_timeout(ctx, &timeout);
对于9600bps的RTU通信,推荐超时设置:
- 字节超时:2-3倍的单字节时间(约2-3ms)
- 响应超时:1.5倍的整帧时间(约400ms)
3. RTU串口连接建立与初始化
3.1 连接建立的整体流程
3.1.1 连接函数调用链
code复制应用层代码
↓
modbus_connect(ctx) // 用户调用连接函数
↓
参数验证(检查ctx是否有效)
↓
ctx->backend->connect(ctx) // 调用后端连接函数
↓
_modbus_rtu_connect(ctx) // RTU模式的具体实现
↓
open(ctx_rtu->device, flags) // 打开串口设备
↓
设置串口参数(波特率、数据位等)
↓
返回文件描述符/句柄
3.2 连接函数源码分析
3.2.1 主连接函数:modbus_connect()
c复制int modbus_connect(modbus_t *ctx)
{
// 1. 参数有效性检查
if (ctx == NULL) {
errno = EINVAL; // 设置错误码:无效参数
return -1;
}
// 2. 调用后端的具体连接函数
return ctx->backend->connect(ctx);
}
3.2.2 RTU连接实现:_modbus_rtu_connect()
c复制/* POSIX系统实现 */
static int _modbus_rtu_connect(modbus_t *ctx)
{
modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
struct termios tios;
// 1. 打开设备
int flags = O_RDWR | O_NOCTTY | O_NDELAY;
ctx->s = open(ctx_rtu->device, flags);
if (ctx->s < 0) {
if (ctx->debug) {
fprintf(stderr,
"ERROR Can't open the device %s (%s)\n",
ctx_rtu->device,
strerror(errno));
}
return -1;
}
// 2. 获取当前串口设置
tcgetattr(ctx->s, &tios);
// 3. 设置波特率
cfsetispeed(&tios, ctx_rtu->baud);
cfsetospeed(&tios, ctx_rtu->baud);
// 4. 设置数据位
tios.c_cflag &= ~CSIZE;
switch (ctx_rtu->data_bit) {
case 5: tios.c_cflag |= CS5; break;
case 6: tios.c_cflag |= CS6; break;
case 7: tios.c_cflag |= CS7; break;
case 8: tios.c_cflag |= CS8; break;
default: return -1;
}
// 5. 设置校验位
if (ctx_rtu->parity == 'N') {
tios.c_cflag &= ~PARENB; // 无校验
} else if (ctx_rtu->parity == 'E') {
tios.c_cflag |= PARENB; // 偶校验
tios.c_cflag &= ~PARODD;
} else if (ctx_rtu->parity == 'O') {
tios.c_cflag |= PARENB; // 奇校验
tios.c_cflag |= PARODD;
}
// 6. 设置停止位
if (ctx_rtu->stop_bit == 1) {
tios.c_cflag &= ~CSTOPB; // 1位停止位
} else {
tios.c_cflag |= CSTOPB; // 2位停止位
}
// 7. 应用设置
tcsetattr(ctx->s, TCSANOW, &tios);
// 8. 清空缓冲区
tcflush(ctx->s, TCIOFLUSH);
return 0;
}
3.3 单片机上的实现差异
3.3.1 单片机串口连接流程
c复制// 单片机版本的_modbus_rtu_connect()
static int _modbus_rtu_connect_stm32(modbus_t *ctx)
{
modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
UART_HandleTypeDef *huart;
if (strcmp(ctx_rtu->device, "UART1") == 0) {
huart = &huart1;
ctx->s = (int)huart; // 将句柄指针转换为整数存储
} else if (strcmp(ctx_rtu->device, "UART2") == 0) {
huart = &huart2;
ctx->s = (int)huart;
} else {
return -1;
}
// 配置串口参数
huart->Instance = USART1;
huart->Init.BaudRate = ctx_rtu->baud;
huart->Init.WordLength = (ctx_rtu->data_bit == 8) ?
UART_WORDLENGTH_8B : UART_WORDLENGTH_9B;
huart->Init.StopBits = (ctx_rtu->stop_bit == 1) ?
UART_STOPBITS_1 : UART_STOPBITS_2;
huart->Init.Parity = (ctx_rtu->parity == 'N') ? UART_PARITY_NONE :
(ctx_rtu->parity == 'E') ? UART_PARITY_EVEN :
UART_PARITY_ODD;
huart->Init.Mode = UART_MODE_TX_RX;
// 初始化串口
if (HAL_UART_Init(huart) != HAL_OK) {
return -1;
}
// 清空接收缓冲区
__HAL_UART_FLUSH_DRREGISTER(huart);
return 0;
}
3.3.2 单片机中的文件描述符替代方案
在单片机中,通常使用以下方式替代文件描述符:
- 使用指针作为标识:
c复制typedef struct {
UART_TypeDef *uart;
IRQn_Type irq;
uint32_t baud_rate;
} uart_handle_t;
- 使用枚举作为标识:
c复制typedef enum {
UART_ID_1 = 0,
UART_ID_2,
UART_ID_3,
UART_ID_MAX
} uart_id_t;
- 直接使用硬件寄存器地址:
c复制ctx->s = (int)USART1; // 将寄存器地址作为标识
3.4 调试与错误排查
3.4.1 连接失败的常见原因
POSIX/Linux系统:
- 设备不存在:路径错误或设备未连接
bash复制$ ls -l /dev/ttyUSB0 # 检查设备是否存在 - 权限不足:需要root权限或加入dialout组
bash复制$ sudo chmod 666 /dev/ttyUSB0 # 临时解决 $ sudo usermod -aG dialout $USER # 永久解决 - 设备被占用:另一个程序正在使用串口
bash复制$ lsof /dev/ttyUSB0 # 查看谁在占用
单片机系统:
- 时钟未使能:串口外设时钟未开启
- 引脚冲突:GPIO引脚被其他功能占用
- 参数错误:波特率等参数超出硬件支持范围
3.4.2 调试技巧
c复制// 在单片机中添加调试输出
int _modbus_rtu_connect(modbus_t *ctx)
{
printf("尝试打开串口: %s\n", ctx_rtu->device);
printf("波特率: %d\n", ctx_rtu->baud);
ctx->s = open(ctx_rtu->device, flags);
printf("打开结果: fd=%d, errno=%d\n", ctx->s, errno);
// 检查系统资源(单片机可能有限制)
if (ctx->s >= MAX_FILE_DESCRIPTORS) {
printf("错误:文件描述符耗尽\n");
return -1;
}
}
4. Modbus请求构建与数据发送流程(写多个线圈)
4.1 整体流程概览
4.1.1 完整的请求-响应流程
code复制应用程序调用modbus_write_bits()
↓
构建请求帧(地址+功能码+数据)
↓
计算字节数并打包线圈数据
↓
添加CRC校验
↓
发送请求(可能涉及RTS控制)
↓
等待并接收响应
↓
验证响应(确认帧)
↓
返回结果给应用程序
4.2 功能码详解:写多个线圈(0x0F)
4.2.1 功能码15的作用
- 功能码:0x0F(十进制15)
- 作用:一次性写入多个线圈(Coil)状态
- 线圈:Modbus中的开关量输出,每个线圈1位(0或1)
4.2.2 请求报文格式(重要!)
code复制RTU模式(二进制):
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| 地址 | 功能码 | 起始地址高 | 起始地址低 | 数量高 | 数量低 | 字节数 | 数据1 | 数据2 | CRC低 | CRC高 |
+--------+--------+-----------+-----------+--------+--------+--------+--------+--------+--------+--------+
| 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | n字节 | ... | 1字节 | 1字节 |
+--------+--------+-----------+-----------+--------+--------+--------+--------+--------+--------+--------+
示例(写11个线圈,起始地址0x0013):
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 0x05 | 0x0F | 0x00 | 0x13 | 0x00 | 0x0B | 0x02 | 0xD1 | 0x05 | CRC低 | CRC高 |
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
↑从站地址5 ↑功能码15 ↑起始地址19(0x13) ↑线圈数11(0x0B) ↑2字节数据 ↑CRC校验
4.3 源码逐步分析
4.3.1 函数入口:modbus_write_bits()
c复制int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src)
{
int rc;
int i;
int byte_count;
int req_length;
uint8_t req[MAX_MESSAGE_LENGTH]; // 请求缓冲区
// 1. 构建请求基础部分(地址+功能码+起始地址+数量)
req_length = ctx->backend->build_request_basis(
ctx, MODBUS_FC_WRITE_MULTIPLE_COILS, addr, nb, req);
// 2. 计算需要多少字节来存储线圈状态
byte_count = (nb / 8) + ((nb % 8) ? 1 : 0);
// 3. 设置字节数到请求中
req[req_length++] = byte_count;
// 4. 打包线圈数据(重点!)
// ... 数据打包代码
// 5. 发送请求并接收响应
rc = send_msg(ctx, req, req_length);
if (rc >= 0) {
uint8_t rsp[MAX_MESSAGE_LENGTH];
// 6. 接收从站的确认响应
rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);
if (rc == -1)
return -1;
// 7. 验证响应是否正确
rc = check_confirmation(ctx, req, rsp, rc);
}
return rc;
}
4.3.2 构建请求基础部分
c复制static int _modbus_rtu_build_request_basis(
modbus_t *ctx, int function, int addr, int nb, uint8_t *req)
{
// 断言:必须已经设置了从站地址
assert(ctx->slave != -1);
// 字节0:从站地址
req[0] = ctx->slave;
// 字节1:功能码
req[1] = function; // 0x0F = 写多个线圈
// 字节2-3:起始地址(16位,大端序:高字节在前)
req[2] = addr >> 8; // 高8位
req[3] = addr & 0x00FF; // 低8位
// 字节4-5:线圈数量(16位,大端序)
req[4] = nb >> 8; // 高8位
req[5] = nb & 0x00FF; // 低8位
// 返回基础部分的长度:6字节
return _MODBUS_RTU_PRESET_REQ_LENGTH; // 6
}
4.3.3 线圈数据打包算法(核心!)
c复制// 假设:nb=11个线圈,src[]包含11个值(0或1)
// 计算需要多少字节:11/8=1余3,所以需要2字节
byte_count = (nb / 8) + ((nb % 8) ? 1 : 0); // = 2
// 设置字节数到请求报文中
req[req_length++] = byte_count; // req[6] = 0x02
// 开始打包数据
int bit_check = 0; // 已处理的位数计数器
int pos = 0; // src数组的当前位置
for (i = 0; i < byte_count; i++) {
int bit;
bit = 0x01; // 从最低位(bit0)开始
req[req_length] = 0; // 清空当前数据字节
// 处理当前字节的8个位(或少于8个,如果nb不是8的倍数)
while ((bit & 0xFF) && (bit_check < nb)) {
// 如果当前线圈状态为1,设置对应位
if (src[pos++] % 2 == 1)
req[req_length] |= bit;
// 否则清除该位(已经是0,但明确操作)
else
req[req_length] &= ~bit;
bit = bit << 1; // 移到下一位
bit_check++; // 已处理位数+1
}
req_length++; // 移动到下一个数据字节
}
4.4 单片机实现要点
4.4.1 数据打包优化(单片机版本)
c复制// 单片机优化版本的数据打包
void pack_coils_to_bytes(const uint8_t *coils, int nb, uint8_t *bytes) {
int byte_index = 0;
int bit_mask = 0x01;
bytes[0] = 0; // 初始化第一个字节
for (int i = 0; i < nb; i++) {
// 设置或清除对应位
if (coils[i]) {
bytes[byte_index] |= bit_mask;
} else {
bytes[byte_index] &= ~bit_mask;
}
// 移动到下一位
bit_mask <<= 1;
// 如果处理完8个线圈,移动到下一个字节
if (bit_mask == 0x100) { // 超过8位
bit_mask = 0x01;
byte_index++;
if (byte_index < ((nb + 7) / 8)) {
bytes[byte_index] = 0; // 初始化下一个字节
}
}
}
}
4.4.2 发送时的时序控制
c复制// 单片机发送函数(不使用系统调用write)
int mcu_uart_send(modbus_t *ctx, const uint8_t *data, int length) {
modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
UART_HandleTypeDef *huart = (UART_HandleTypeDef *)ctx->s;
// 1. 计算发送所需时间(用于延时)
uint32_t total_time_us = ctx_rtu->onebyte_time * length;
// 2. 如果有RTS控制,先使能发送
if (ctx_rtu->rts != MODBUS_RTU_RTS_NONE) {
ctx_rtu->set_rts(ctx, 1);
HAL_Delay(ctx_rtu->rts_delay / 1000); // 转换为毫秒
}
// 3. 发送数据(阻塞方式)
HAL_StatusTypeDef status = HAL_UART_Transmit(huart, (uint8_t*)data, length, 1000);
// 4. 等待数据完全发送
while (huart->gState != HAL_UART_STATE_READY) {
// 等待发送完成
}
// 5. 恢复RTS(切换回接收)
if (ctx_rtu->rts != MODBUS_RTU_RTS_NONE) {
HAL_Delay(ctx_rtu->rts_delay / 1000);
ctx_rtu->set_rts(ctx, 0);
}
return (status == HAL_OK) ? length : -1;
}
4.5 调试技巧与常见问题
4.5.1 调试报文内容
c复制// 在单片机中打印调试信息
void debug_print_request(uint8_t *req, int length) {
printf("发送请求(%d字节):", length);
for (int i = 0; i < length; i++) {
printf("%
