[特殊字符] libmodbus RTU 源码情景分析 - 发送请求

第一章:RTU 模式初始化与上下文创建

一、RTU 模式创建函数 modbus_new_rtu()

1.1 函数原型和参数
cs 复制代码
modbus_t *modbus_new_rtu(
    const char *device,   // 串口设备名(如 "/dev/ttyUSB0")
    int baud,            // 波特率(如 9600、115200)
    char parity,         // 奇偶校验:'N'=无,'E'=偶,'O'=奇
    int data_bit,        // 数据位(通常为 8)
    int stop_bit         // 停止位(1 或 2)
)
1.2 内存分配流程
cs 复制代码
// 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.3 设备名存储
cs 复制代码
// 分配设备名内存(长度+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

二、串口参数配置(针对单片机)

2.1 基本参数设置
cs 复制代码
// 设置数据位和停止位(这两项是单片机通信的关键)
ctx_rtu->data_bit = data_bit;  // 通常为 8
ctx_rtu->stop_bit = stop_bit;  // 通常为 1
2.2 单字节传输时间计算

重要知识点:RTU 协议需要精确的时间控制,这个时间用于帧间隔检测。

cs 复制代码
// 计算传输一个字节需要的时间(微秒)
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):

cs 复制代码
单字节时间 = 1000000 × (1 + 8 + 0 + 1) / 9600
          = 1000000 × 10 / 9600
          ≈ 1041.67 微秒
≈ 1.04ms
2.3 RTS(请求发送)相关设置
cs 复制代码
// 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;

三、RTU 后端函数表(核心操作集)

3.1 后端结构体定义
cs 复制代码
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                      // 释放资源
};
3.2 RTU 协议格式说明
cs 复制代码
RTU 帧格式:
+--------+--------+--------+----------+------------+--------+--------+
| 地址   | 功能码 | 数据   | ...      | 数据       | CRC低  | CRC高  |
+--------+--------+--------+----------+------------+--------+--------+
| 1字节  | 1字节  | n字节  | ...      | ...        | 1字节  | 1字节  |
+--------+--------+--------+----------+------------+--------+--------+

总长度:1 + 1 + n + 2 = n+4 字节

四、单片机应用的关键要点

4.1 时间控制(重要!)
  • RTU 帧间需要有 3.5个字符时间 的间隔

  • 帧内字符间不超过 1.5个字符时间

  • 计算公式:

    cs 复制代码
    帧间隔时间 = 3.5 × 单字节时间
              = 3.5 × (1+数据位+校验位+停止位) ÷ 波特率
4.2 内存管理注意事项
cs 复制代码
// 初始化成功检查顺序:
// 1. ctx (主结构体) 分配
// 2. ctx->backend_data (RTU数据) 分配  
// 3. ctx_rtu->device (设备名) 分配

// 失败时需要反向释放:
if (ctx_rtu->device == NULL) {
    modbus_free(ctx);  // 内部会释放 backend_data
    return NULL;
}
4.3 针对单片机的适配建议
cs 复制代码
// 1. 串口初始化(单片机特有)
void modbus_rtu_serial_init(modbus_t *ctx) {
    modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
    
    // 配置单片机串口(根据平台实现)
    serial_config(ctx_rtu->device, 
                  ctx_rtu->baud,
                  ctx_rtu->data_bit,
                  ctx_rtu->parity,
                  ctx_rtu->stop_bit);
}

// 2. 重写 RTS 控制(如果需要)
void modbus_rtu_set_rts(modbus_t *ctx, int mode) {
    modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
    
    if (mode == MODBUS_RTU_RTS_UP) {
        // 设置单片机 GPIO 为高电平
        GPIO_SetHigh(RTS_PIN);
    } else {
        // 设置单片机 GPIO 为低电平  
        GPIO_SetLow(RTS_PIN);
    }
    
    // 必要的延时(确保电平稳定)
    delay_us(ctx_rtu->rts_delay);
}

五、代码流程图

cs 复制代码
modbus_new_rtu()
    ├── 分配 modbus_t 结构体
    ├── 初始化通用部分 (modbus_init_common)
    ├── 设置后端类型为 RTU
    ├── 分配 RTU 专用数据
    ├── 复制设备名
    ├── 设置串口参数 (数据位/停止位)
    ├── 计算单字节传输时间 ← 重要!
    ├── 设置 RTS 相关参数
    └── 返回上下文指针

第二章:从站地址设置与 Modbus 上下文结构详解

2.1 Modbus 主/从模式基础概念

2.1.1 两种工作模式
cs 复制代码
Modbus 通信模式:
├── 主站模式(Master)
│   ├── 发起请求
│   ├── 控制通信时序
│   └── 一个主站可对应多个从站
│
└── 从站模式(Slave)
    ├── 响应请求
    ├── 被动接收命令
    └── 每个从站有唯一地址(1-247)
2.1.2 地址范围规则
地址范围 用途 说明
0 广播地址 所有从站都能接收,但不回复响应
1-247 正常从站地址 标准Modbus RTU地址范围
248-255 保留地址 特殊用途或扩展地址

2.2 Modbus 上下文结构体(核心数据结构)

2.2.1 结构体定义分析
cs 复制代码
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专用数据
};
2.2.2 成员详细解释

1. slave 成员

cs 复制代码
int slave;  // 取值范围:0-255
  • 主站模式时:表示要访问的目标从站地址

  • 从站模式时:表示自身的地址

  • 特殊值 0:广播地址

2. 文件描述符 s

cs 复制代码
int s;  // 在单片机中通常是串口句柄
  • 在 RTU 模式下:串口文件描述符

  • 在 TCP 模式下:socket 描述符

  • 单片机中:可能是 UART 句柄编号

3. 超时设置(关键参数)

cs 复制代码
struct timeval response_timeout;  // 完整响应的超时时间
struct timeval byte_timeout;      // 单个字节接收的超时时间

timeval 结构体定义

cs 复制代码
struct timeval {
    time_t      tv_sec;     // 秒
    suseconds_t tv_usec;    // 微秒
};

4. 后端框架指针

cs 复制代码
const modbus_backend_t *backend;  // 指向第一章讲的函数表
void *backend_data;               // 指向第一章讲的 modbus_rtu_t 结构

2.3 设置从站地址函数详解

2.3.1 主函数:modbus_set_slave()
cs 复制代码
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);
}

调用示例

cs 复制代码
// 设置从站地址为 1
if (use_backend == RTU) {
    modbus_set_slave(ctx, SERVER_ID);  // SERVER_ID = 1
}
2.3.2 实际实现函数:_modbus_set_slave()
cs 复制代码
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 完整的调用链
cs 复制代码
应用层代码
    ↓
modbus_set_slave(ctx, 1)          // 用户调用
    ↓
检查 ctx 是否为空                  // 参数验证
    ↓
ctx->backend->set_slave(ctx, 1)   // 调用后端函数
    ↓
_modbus_set_slave(ctx, 1)         // 实际执行
    ↓
检查 quirk 标志确定最大地址        // 兼容性处理
    ↓
验证地址范围 0 <= slave <= max    // 范围检查
    ↓
设置 ctx->slave = 1              // 成功设置
    ↓
返回 0                            // 成功
2.4.2 错误处理流程
cs 复制代码
应用层代码
    ↓
modbus_set_slave(NULL, 1)         // 错误:ctx为空
    ↓
设置 errno = EINVAL               // 错误码
    ↓
返回 -1                           // 失败

或者:
    ↓
modbus_set_slave(ctx, 300)        // 错误:地址超范围
    ↓
ctx->backend->set_slave(ctx, 300)
    ↓
_modbus_set_slave(ctx, 300)
    ↓
检查 quirk 标志:假设 max=247
    ↓
300 > 247,地址无效
    ↓
设置 errno = EINVAL
    ↓
返回 -1

2.5 单片机应用注意事项

2.5.1 地址设置时机
cs 复制代码
// 正确的使用顺序
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.5.2 广播地址的特殊处理
cs 复制代码
// 广播地址(地址0)的使用
modbus_set_slave(ctx, MODBUS_BROADCAST_ADDRESS);  // 地址0

// 广播特点:
// 1. 所有从站都会接收命令
// 2. 没有从站会回复响应
// 3. 用于同时控制多个从站
// 4. 只能用于写操作(不能读)
2.5.3 超时设置(单片机中重要!)
cs 复制代码
// 设置超时(针对单片机可能调整)
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);

单片机推荐超时设置

cs 复制代码
// 对于 9600bps 的 RTU:
// 计算一个字节时间:1.04ms
// 一帧最大长度:256字节 ≈ 266ms
// 建议设置:
// 字节超时:2-3倍的单字节时间(2-3ms)
// 响应超时:1.5倍的整帧时间(400ms)

2.6 常见问题与调试

2.6.1 地址设置失败的常见原因
cs 复制代码
// 原因1:上下文未初始化
modbus_t *ctx = NULL;
modbus_set_slave(ctx, 1);  // 错误!ctx为NULL

// 原因2:地址超出范围
modbus_set_slave(ctx, 300);  // 错误!标准范围是0-247

// 原因3:未考虑 quirk 标志
// 如果设备支持扩展地址,需要设置 quirk 标志
2.6.2 调试技巧
cs 复制代码
// 1. 开启调试模式
modbus_set_debug(ctx, TRUE);  // 打印调试信息

// 2. 检查错误代码
int result = modbus_set_slave(ctx, 300);
if (result == -1) {
    printf("错误代码: %d, 错误信息: %s\n", errno, strerror(errno));
    // errno = EINVAL 表示参数无效
}

// 3. 验证设置是否成功
modbus_set_slave(ctx, 1);
// 可以添加日志打印 ctx->slave 的值

2.7 本章核心知识点总结

  1. Modbus 地址系统

    • 0为广播地址,1-247为正常从站地址

    • 广播地址用于写操作,不产生响应

  2. 上下文结构体 (struct modbus)

    • 包含通信的所有状态信息

    • slave 成员的双重含义(主站目标/从站自身)

    • 超时设置对稳定性至关重要

  3. 地址设置流程

    • 通过后端函数表调用具体实现

    • 支持 quirk 标志扩展地址范围

    • 严格的参数验证

  4. 单片机应用要点

    • 合理设置超时时间(基于波特率计算)

    • 注意主/从模式的地址含义不同

    • 广播地址的特殊用途

  5. 错误处理

    • 使用 errno 获取错误原因

    • EINVAL 表示参数无效

    • 检查函数返回值判断成功与否

第三章:RTU 串口连接建立与初始化

3.1 连接建立的整体流程

3.1.1 连接函数调用链
cs 复制代码
应用层代码
    ↓
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()
cs 复制代码
int modbus_connect(modbus_t *ctx)
{
    // 1. 参数有效性检查
    if (ctx == NULL) {
        errno = EINVAL;  // 设置错误码:无效参数
        return -1;
    }
    
    // 2. 调用后端的具体连接函数
    return ctx->backend->connect(ctx);
}

代码解析

  1. 参数验证 :首先检查上下文指针 ctx 是否为空

  2. 错误处理 :如果为空,设置 errno = EINVAL(参数无效)

  3. 调用后端:通过函数指针调用具体的后端连接函数

  4. 返回值:返回后端函数的结果(成功为0,失败为-1)

3.2.2 RTU 连接实现:_modbus_rtu_connect()
cs 复制代码
/* POSIX 系统实现 */
static int _modbus_rtu_connect(modbus_t *ctx)
{
    // 1. 打开串口设备文件
    ctx->s = open(ctx_rtu->device, flags);
    
    // 2. 检查打开是否成功
    if (ctx->s < 0) {
        // 3. 打开失败的处理
        if (ctx->debug) {
            // 如果开启了调试模式,打印错误信息
            fprintf(stderr,
                "ERROR Can't open the device %s (%s)\n",
                ctx_rtu->device,
                strerror(errno));
        }
        return -1;  // 返回失败
    }
    
    // 4. 配置串口参数(波特率、数据位、停止位、校验位等)
    
    // 5. 清空缓冲区等其他初始化操作
    
    return 0;  // 返回成功
}

3.3 关键概念详解

3.3.1 文件描述符(File Descriptor)

在 POSIX 系统中

cs 复制代码
ctx->s = open(ctx_rtu->device, flags);
  • open():系统调用,打开设备文件

  • ctx->s:文件描述符(整数),后续所有串口操作都使用这个值

  • 文件描述符值:

    • >= 0:有效的文件描述符

    • < 0:表示错误(通常是-1)

在单片机中(重要区别!):

cs 复制代码
// 单片机中没有文件描述符概念,通常是:
ctx->s = uart_open(ctx_rtu->device);  // 返回UART句柄
// 或者直接使用串口编号
ctx->s = UART1_HANDLE;  // 固定值
3.3.2 open() 函数的 flags 参数
cs 复制代码
// 常见的串口打开标志(POSIX)
#define O_RDWR      0x0002      // 读写模式
#define O_NOCTTY    0x0080      // 不分配控制终端
#define O_NDELAY    0x0004      // 非阻塞模式

// 典型组合:
int flags = O_RDWR | O_NOCTTY | O_NDELAY;
3.3.3 错误处理机制

1. 错误代码传递

cs 复制代码
if (ctx->s < 0) {
    // open() 失败时,系统会设置 errno
    // errno 是全局变量,记录最后一次系统调用的错误
    return -1;
}

2. 调试信息输出

cs 复制代码
if (ctx->debug) {
    fprintf(stderr, "ERROR Can't open the device %s (%s)\n",
            ctx_rtu->device,
            strerror(errno));
}
  • ctx->debug:调试标志,由用户设置

  • strerror(errno):将错误代码转换为可读的错误信息

常见的 errno 错误代码

错误代码 宏定义 含义
2 ENOENT 文件/设备不存在
13 EACCES 权限不足
16 EBUSY 设备忙(被占用)
19 ENODEV 设备不存在
22 EINVAL 参数无效

3.4 单片机上的实现差异

3.4.1 单片机串口连接流程
cs 复制代码
// 单片机版本的 _modbus_rtu_connect()
static int _modbus_rtu_connect_stm32(modbus_t *ctx)
{
    modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
    
    // 1. 初始化串口硬件(没有open()系统调用)
    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;
    }
    
    // 2. 配置串口参数
    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;
    
    // 3. 初始化串口
    if (HAL_UART_Init(huart) != HAL_OK) {
        return -1;
    }
    
    // 4. 清空接收缓冲区
    __HAL_UART_FLUSH_DRREGISTER(huart);
    
    return 0;
}
3.4.2 单片机中的文件描述符替代方案
cs 复制代码
// 方案1:使用指针作为标识
typedef struct {
    UART_TypeDef *uart;
    IRQn_Type irq;
    uint32_t baud_rate;
} uart_handle_t;

// 方案2:使用枚举作为标识
typedef enum {
    UART_ID_1 = 0,
    UART_ID_2,
    UART_ID_3,
    UART_ID_MAX
} uart_id_t;

// 方案3:直接使用硬件寄存器地址
ctx->s = (int)USART1;  // 将寄存器地址作为标识

3.5 完整的连接初始化流程

3.5.1 POSIX 系统完整流程
cs 复制代码
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) {
        // 错误处理
        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.5.2 单片机中的关键初始化步骤
cs 复制代码
// 单片机串口初始化核心步骤
int mcu_uart_init(modbus_t *ctx)
{
    // 1. 使能时钟(STM32为例)
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 2. 配置GPIO引脚
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;  // TX/RX
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 3. 配置串口中断(如果需要)
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
    
    return 0;
}

3.6 调试与错误排查

3.6.1 连接失败的常见原因

POSIX/Linux 系统

  1. 设备不存在:路径错误或设备未连接

    bash

    复制代码
    $ ls -l /dev/ttyUSB0  # 检查设备是否存在
  2. 权限不足:需要root权限或加入dialout组

    bash

    复制代码
    $ sudo chmod 666 /dev/ttyUSB0  # 临时解决
    $ sudo usermod -aG dialout $USER  # 永久解决
  3. 设备被占用:另一个程序正在使用串口

    bash

    复制代码
    $ lsof /dev/ttyUSB0  # 查看谁在占用

单片机系统

  1. 时钟未使能:串口外设时钟未开启

  2. 引脚冲突:GPIO引脚被其他功能占用

  3. 参数错误:波特率等参数超出硬件支持范围

3.6.2 调试技巧
cs 复制代码
// 1. 在单片机中添加调试输出
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);
    
    // 2. 检查系统资源(单片机可能有限制)
    if (ctx->s >= MAX_FILE_DESCRIPTORS) {
        printf("错误:文件描述符耗尽\n");
        return -1;
    }
}

3.7 本章核心知识点总结

  1. 连接函数分层结构

    • 应用层调用 modbus_connect()

    • 中间层调用后端函数指针

    • 底层实现 _modbus_rtu_connect()

  2. 文件描述符概念

    • POSIX 系统中标识打开文件的整数

    • 单片机中通常用硬件句柄或寄存器地址代替

    • 负值表示错误

  3. 错误处理机制

    • 使用 errno 传递错误代码

    • 通过 strerror() 转换为可读信息

    • 支持调试模式输出详细信息

  4. 单片机实现差异

    • 没有 open() 系统调用

    • 需要直接配置硬件寄存器

    • 关注时钟使能、引脚配置等底层操作

  5. 串口参数配置

    • 波特率、数据位、停止位、校验位

    • 需要与从站设备完全一致

    • 配置错误会导致通信失败

  6. 调试建议

    • 在关键步骤添加调试输出

    • 检查权限和设备状态

    • 验证参数的有效性

第四章:Modbus请求构建与数据发送流程(写多个线圈)

4.1 整体流程概览

4.1.1 完整的请求-响应流程
cs 复制代码
应用程序调用 modbus_write_bits()
    ↓
构建请求帧(地址+功能码+数据)
    ↓
计算字节数并打包线圈数据
    ↓
添加CRC校验
    ↓
发送请求(可能涉及RTS控制)
    ↓
等待并接收响应
    ↓
验证响应(确认帧)
    ↓
返回结果给应用程序

4.2 功能码详解:写多个线圈(0x0F)

4.2.1 功能码15的作用
  • 功能码:0x0F(十进制15)

  • 作用:一次性写入多个线圈(Coil)状态

  • 线圈:Modbus中的开关量输出,每个线圈1位(0或1)

4.2.2 请求报文格式(重要!)
cs 复制代码
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()
cs 复制代码
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;
}

参数说明

  • addr:起始地址(如0x0013)

  • nb:要写入的线圈数量(如11)

  • src:线圈状态数组,每个元素一个线圈(0=OFF,1=ON)

4.3.2 构建请求基础部分
cs 复制代码
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
}

重要概念:大端序(Big-Endian)

cs 复制代码
16位地址0x0013在报文中的存储:
高8位:0x00 → req[2]
低8位:0x13 → req[3]

为什么这样设计?Modbus协议规定使用大端序(网络字节序)
单片机通常是小端序,需要转换
4.3.3 线圈数据打包算法(核心!)
cs 复制代码
// 假设: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++;  // 移动到下一个数据字节
}

打包示例

cs 复制代码
线圈状态数组src[] = [1,0,1,0,0,1,0,1,0,1,1]
对应线圈地址:0x0013, 0x0014, ..., 0x001D

打包过程:
第一个字节(i=0):
  bit=0x01(00000001) → 处理线圈0x0013(1) → 设置bit0 → 字节=00000001
  bit=0x02(00000010) → 处理线圈0x0014(0) → 清除bit1 → 字节=00000001
  bit=0x04(00000100) → 处理线圈0x0015(1) → 设置bit2 → 字节=00000101
  ... 依次处理8个线圈
  最终第一个字节:11010001 = 0xD1

第二个字节(i=1):
  处理剩余的3个线圈
  bit=0x01 → 处理线圈0x001B(0) → 清除bit0 → 字节=00000000
  bit=0x02 → 处理线圈0x001C(1) → 设置bit1 → 字节=00000010
  bit=0x04 → 处理线圈0x001D(1) → 设置bit2 → 字节=00000110
  最终第二个字节:00000101 = 0x05(注意:bit2=1, bit0=1?这里需要验证)

注意:实际打包顺序是从字节的最低位(bit0)开始,对应第一个线圈

4.4 发送请求流程

4.4.1 发送主函数:send_msg()
cs 复制代码
static int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length)
{
    int rc;
    int i;
    
    // 1. 发送前预处理(主要是添加CRC校验)
    msg_length = ctx->backend->send_msg_pre(msg, msg_length);
    
    // 2. 调试模式:打印发送的报文
    if (ctx->debug) {
        for (i = 0; i < msg_length; i++)
            printf("[%.2X]", msg[i]);
        printf("\n");
    }
    
    // 3. 发送数据(支持错误恢复模式)
    do {
        rc = ctx->backend->send(ctx, msg, msg_length);
        if (rc == -1) {
            _error_print(ctx, NULL);
            if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {
                // 错误恢复处理:可能重新连接等
            }
        }
    } while ( /* 错误恢复模式下重试条件 */ );
    
    return rc;
}
4.4.2 发送前预处理:添加CRC校验
cs 复制代码
static int _modbus_rtu_send_msg_pre(uint8_t *req, int req_length)
{
    // 计算CRC16校验值(从地址字节开始,到数据结束)
    uint16_t crc = crc16(req, req_length);
    
    /* 根据MODBUS规范,CRC的低字节在前,高字节在后 */
    req[req_length++] = crc & 0x00FF;   // 低字节
    req[req_length++] = crc >> 8;       // 高字节
    
    return req_length;  // 返回新的长度(原长度+2)
}

CRC计算要点

  • 计算范围:从从站地址到最后一个数据字节

  • 字节顺序:先低字节,后高字节

  • 算法:Modbus使用CRC-16-IBM(多项式0x8005)

4.4.3 RTU发送函数(包含RTS控制)
cs 复制代码
static size_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
#if defined(_WIN32)
    // Windows实现(略)
#else
    // Linux/POSIX实现
#if HAVE_DECL_TIOCM_RTS  // 如果支持RTS控制
    modbus_rtu_t *ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
    
    if (ctx_rtu->rts != MODBUS_RTU_RTS_NONE) {
        size_t size;
        
        if (ctx->debug) {
            fprintf(stderr, "Sending request using RTS signal\n");
        }
        
        // 1. 设置RTS信号(对于RS485,控制发送使能)
        ctx_rtu->set_rts(ctx, ctx_rtu->rts == MODBUS_RTU_RTS_UP);
        
        // 2. 延时,确保信号稳定
        usleep(ctx_rtu->rts_delay);
        
        // 3. 发送数据
        size = write(ctx->s, req, req_length);
        
        // 4. 延时,等待数据完全发送
        // 公式:每个字节时间×字节数 + RTS切换延时
        usleep(ctx_rtu->onebyte_time * req_length + ctx_rtu->rts_delay);
        
        // 5. 恢复RTS信号(关闭发送使能,切换回接收)
        ctx_rtu->set_rts(ctx, ctx_rtu->rts != MODBUS_RTU_RTS_UP);
        
        return size;
    } else {
        // 不使用RTS,直接发送
        return write(ctx->s, req, req_length);
    }
#endif
}

单片机中的RTS实现(重要!):

cs 复制代码
// 单片机版本的set_rts函数(控制RS485收发切换)
void mcu_set_rts(modbus_t *ctx, int on) {
    if (on) {
        // 拉高引脚,使能发送(RS485的DE引脚)
        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);
    }
}

4.5 接收响应与确认

4.5.1 接收确认响应
cs 复制代码
// 接收响应报文
rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);

// 验证响应
rc = check_confirmation(ctx, req, rsp, rc);
确认响应格式(写多个线圈的响应):
cs 复制代码
+--------+--------+--------+--------+--------+--------+--------+
| 地址   | 功能码 | 起始地址高 | 起始地址低 | 数量高 | 数量低 | CRC   |
+--------+--------+-----------+-----------+--------+--------+--------+
| 1字节  | 1字节  | 1字节     | 1字节     | 1字节  | 1字节  | 2字节  |
+--------+--------+-----------+-----------+--------+--------+--------+

验证内容

  1. 从站地址是否匹配

  2. 功能码是否相同(还是0x0F)

  3. 起始地址是否一致

  4. 线圈数量是否一致

4.6 单片机实现要点

4.6.1 数据打包优化(单片机版本)
cs 复制代码
// 单片机优化版本的数据打包
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.6.2 发送时的时序控制
cs 复制代码
// 单片机发送函数(不使用系统调用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. 等待数据完全发送
    // 方法1:计算延时
    // HAL_Delay(total_time_us / 1000 + 1);  // +1确保完全发送
    
    // 方法2:检查发送完成标志(更准确)
    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.7 调试技巧与常见问题

4.7.1 调试报文内容
cs 复制代码
// 在单片机中打印调试信息
void debug_print_request(uint8_t *req, int length) {
    printf("发送请求(%d字节):", length);
    for (int i = 0; i < length; i++) {
        printf("%02X ", req[i]);
    }
    printf("\n");
    
    // 解析报文内容
    printf("解析:\n");
    printf("  从站地址: %d\n", req[0]);
    printf("  功能码: 0x%02X\n", req[1]);
    printf("  起始地址: 0x%02X%02X = %d\n", req[2], req[3], (req[2]<<8)|req[3]);
    printf("  线圈数量: %d\n", (req[4]<<8)|req[5]);
    printf("  字节数: %d\n", req[6]);
    
    // 打印线圈数据
    printf("  线圈数据: ");
    for (int i = 7; i < 7 + req[6]; i++) {
        printf("%02X ", req[i]);
    }
    printf("\n");
    
    // 打印CRC
    printf("  CRC: %02X %02X\n", req[length-2], req[length-1]);
}
4.7.2 常见问题排查

问题1:数据打包错误

cs 复制代码
症状:从站接收到的线圈状态不正确
排查:
  1. 检查字节顺序(大端序/小端序)
  2. 验证位打包顺序(最低位对应第一个线圈)
  3. 检查字节数计算:(nb+7)/8

问题2:CRC校验失败

cs 复制代码
症状:从站不响应或返回异常响应
排查:
  1. 验证CRC计算范围(从地址到数据)
  2. 检查CRC字节顺序(低字节在前)
  3. 使用CRC计算工具对比

问题3:RTS时序问题

cs 复制代码
症状:数据发送不完整或冲突
排查:
  1. 检查RTS切换延时是否足够
  2. 验证发送完成后再切换RTS
  3. 使用逻辑分析仪观察时序

4.8 本章核心知识点总结

  1. 请求报文结构

    • 6字节基础头 + 1字节数 + n字节数据 + 2字节CRC

    • 使用大端序存储16位值

  2. 数据打包算法

    • 每个线圈对应1位(0或1)

    • 从字节的最低位(bit0)开始打包

    • 需要计算字节数:(nb+7)/8

  3. CRC校验

    • 计算从地址到数据的所有字节

    • 低字节在前,高字节在后

    • 使用Modbus标准CRC-16算法

  4. 发送流程

    • 先添加CRC,再发送

    • RTS控制用于RS485收发切换

    • 需要精确的时序控制

  5. 单片机实现要点

    • 优化数据打包算法

    • 实现准确的RTS控制

    • 确保发送完成后再切换状态

  6. 调试方法

    • 打印完整报文进行验证

    • 使用串口调试助手辅助测试

    • 逐步验证每个环节

🔗 核心知识链总览

我为你串联整个学习过程的知识点,从零开始理解libmodbus RTU的工作机制:

一、三层架构思想(贯穿始终的核心)

cs 复制代码
应用层(用户代码)
    ↓
Modbus API层(modbus_xxx函数)
    ↓
后端抽象层(函数指针表)
    ↓
具体实现层(RTU/TCP实现)

关键理解 :每个modbus_t结构体都有一个backend指针,指向具体的函数实现表,这种设计实现了"一次编写,多平台运行"。

二、完整工作流程串联

cs 复制代码
1. 创建上下文(modbus_new_rtu)
   ├── 分配modbus_t主结构
   ├── 分配modbus_rtu_t专用数据
   ├── 计算单字节时间(通信时序基础)
   └── 设置后端函数表

2. 配置参数(modbus_set_slave)
   ├── 设置主/从站地址
   ├── 地址范围验证(0-247标准,0为广播)
   └── 存储在ctx->slave中

3. 建立连接(modbus_connect)
   ├── 打开串口设备(POSIX:open())
   ├── 单片机:初始化UART硬件
   ├── 配置串口参数(波特率、数据位、停止位、校验位)
   └── 获得文件描述符/句柄

4. 构建请求(以写线圈为例)
   ├── 功能码:0x0F(写多个线圈)
   ├── 地址大端序存储(高字节在前)
   ├── 线圈数据位打包(从字节最低位开始)
   ├── 计算CRC(低字节在前)
   └── RTS控制(RS485收发切换)

5. 发送请求
   ├── 可能的重试机制(错误恢复模式)
   ├── RTS时序控制(发送前使能,发送后延时再关闭)
   └── 单片机需确保发送完成再切换

6. 接收响应
   ├── 验证确认帧(地址、功能码、地址、数量)
   └── 错误处理和重试

三、关键数据结构关联

cs 复制代码
struct modbus (通用上下文)
    ├── int slave           ← 第二章讲解(地址设置)
    ├── int s              ← 第三章讲解(串口句柄)
    ├── backend指针        ← 第一章讲解(指向函数表)
    └── backend_data指针   ← 第一章讲解(指向RTU数据)
        ↓
struct modbus_rtu_t (RTU专用数据) ← 第一章讲解
    ├── char *device       (串口设备名)
    ├── int baud          (波特率)
    ├── uint32_t onebyte_time ← 核心!(单字节传输时间)
    └── RTS相关参数        (RS485控制)

四、时间计算的核心公式串联

  1. 单字节时间(第一章基础):

    cs 复制代码
    单字节时间(微秒) = 1000000 × (起始位1 + 数据位 + 校验位0/1 + 停止位) ÷ 波特率
  2. 帧间隔时间(RTU协议要求):

    cs 复制代码
    帧间隔 = 3.5 × 单字节时间
    (用于区分不同帧)
  3. RTS延时时间(第四章应用):

    cs 复制代码
    发送前延时 = RTS延时(通常=单字节时间)
    发送后延时 = 数据发送时间 + RTS延时
    数据发送时间 = 单字节时间 × 字节数

五、单片机实现的特殊性串联

  1. 无文件描述符 :用UART句柄或寄存器地址代替ctx->s

  2. 无系统调用 :直接操作硬件寄存器而非open()/write()

  3. RTS控制:自己实现GPIO控制收发切换

  4. 延时精度:需要高精度定时器保证时序

  5. 内存管理:静态分配代替动态分配更安全

六、从理论到实践的知识点应用

  1. 初始化阶段

    • 使用modbus_new_rtu()创建上下文

    • modbus_set_slave()设置地址

    • 调用modbus_connect()建立连接

  2. 通信阶段

    • 构建请求时注意大端序存储16位值

    • 数据打包时从字节最低位开始

    • CRC计算范围:从地址到数据,低字节在前

    • RS485通信必须控制RTS时序

  3. 调试阶段

    • 开启调试模式查看原始报文

    • 验证每个环节:地址→数据→CRC

    • 使用时序分析工具验证时间间隔

七、常见错误排查链

cs 复制代码
通信失败
    ↓
检查连接(第三章)
    ├── 设备路径是否正确?
    ├── 波特率等参数是否匹配?
    └── 权限是否足够?
    ↓
检查地址(第二章)
    ├── 从站地址是否正确?
    ├── 是否设置了slave?
    └── 地址是否在有效范围?
    ↓
检查数据(第四章)
    ├── 数据打包顺序是否正确?
    ├── CRC计算是否正确?
    └── RTS时序是否准确?
    ↓
检查时序(第一章基础)
    ├── 单字节时间计算是否正确?
    ├── 帧间隔是否足够?
    └── 响应超时设置是否合理?

八、学习路径建议

  1. 先理解架构:三层分离思想

  2. 再掌握流程:创建→配置→连接→通信

  3. 重点掌握公式:时间计算是RTU核心

  4. 实践调试:从简单到复杂逐步验证

  5. 单片机适配:理解差异,针对性修改

九、核心记忆点

  1. 一个核心结构modbus_t 贯穿始终

  2. 两个关键指针backendbackend_data

  3. 三个时间概念:单字节时间、帧间隔、RTS延时

  4. 四个通信阶段:初始化、连接、请求、响应

  5. 五个调试要点:地址、数据、CRC、时序、硬件


🎯 终极总结:一句话记住libmodbus RTU

"通过三层架构抽象,使用时间精确控制的串口协议,按照Modbus规范打包数据,实现可靠的半双工通信。"


📚 完整知识树

cs 复制代码
libmodbus RTU
├── 架构设计
│   ├── 三层抽象(应用-抽象-实现)
│   ├── 函数指针表机制
│   └── 上下文隔离设计
│
├── 核心数据结构
│   ├── modbus_t(通用上下文)
│   ├── modbus_backend_t(函数表)
│   └── modbus_rtu_t(RTU数据)
│
├── 初始化流程
│   ├── 内存分配(三级分配)
│   ├── 参数计算(单字节时间)
│   └── 后端绑定(RTU函数表)
│
├── 通信流程
│   ├── 连接建立(open/硬件初始化)
│   ├── 请求构建(大端序+位打包)
│   ├── 校验计算(CRC低字节在前)
│   ├── 发送控制(RTS时序)
│   └── 响应处理(确认验证)
│
└── 单片机适配
    ├── 硬件直接操作(无系统调用)
    ├── 静态内存管理
    ├── 精确时序控制
    └── GPIO手动控制(RTS/485)
相关推荐
fen_fen11 小时前
Oracle建表语句示例
数据库·oracle
砚边数影13 小时前
数据可视化入门:Matplotlib 基础语法与折线图绘制
数据库·信息可视化·matplotlib·数据可视化·kingbase·数据库平替用金仓·金仓数据库
orange_tt13 小时前
Djiango配置Celery
数据库·sqlite
云小逸14 小时前
【nmap源码学习】 Nmap网络扫描工具深度解析:从基础参数到核心扫描逻辑
网络·数据库·学习
肉包_51114 小时前
两个数据库互锁,用全局变量互锁会偶发软件卡死
开发语言·数据库·c++
霖霖总总14 小时前
[小技巧64]深入解析 MySQL InnoDB 的 Checkpoint 机制:原理、类型与调优
数据库·mysql
此刻你15 小时前
常用的 SQL 语句
数据库·sql·oracle
それども16 小时前
分库分表的事务问题 - 怎么实现事务
java·数据库·mysql
·云扬·16 小时前
MySQL Binlog 配置指南与核心作用解析
数据库·mysql·adb
天空属于哈夫克316 小时前
Java 版:利用外部群 API 实现自动“技术开课”倒计时提醒
数据库·python·mysql