1. ModbusSlaveRTU 库概述:面向实时嵌入式系统的 RTU 从机实现

ModbusSlaveRTU 是一个专为资源受限嵌入式环境设计的轻量级、可重入、线程安全的 Modbus RTU 从机协议栈。它不依赖特定硬件抽象层(HAL),而是以纯 C 实现,通过标准串口驱动接口(如 read() / write() 或 HAL_UART_Receive_IT + HAL_UART_Transmit_IT)与底层通信外设解耦;其核心设计目标是满足工业现场对确定性响应时间、多实例共存及与实时操作系统(RTOS)原生集成的严苛要求。

该库并非传统“单片机裸机轮询式”Modbus 实现,而是采用 事件驱动 + 消息队列 架构:所有接收到的 Modbus 请求帧被解析后封装为结构化消息,投递至用户配置的消息队列(如 FreeRTOS 的 QueueHandle_t 、Zephyr 的 k_msgq 或裸机环形缓冲区);主业务逻辑或专用处理任务从此队列中取出请求,执行寄存器读写、状态判断等应用层操作,并将构造好的响应消息回传至库的发送队列,最终由底层串口驱动完成物理层发送。这种设计彻底分离了协议解析、物理传输与业务逻辑,使系统具备高内聚、低耦合、易测试、可扩展的工程特性。

在实际工业网关、PLC 模块、智能传感器节点等场景中,常需在同一 MCU 上运行多个 Modbus 从机实例——例如:一个实例映射 RS-485 总线上的保持寄存器(Holding Register),另一个实例管理输入寄存器(Input Register)并触发中断事件,第三个实例专用于诊断寄存器(Diagnostic Register)的调试访问。ModbusSlaveRTU 原生支持多实例并发运行,每个实例拥有独立的地址空间、超时参数、错误计数器及消息队列句柄,无需修改源码即可通过 ModbusSlaveRTU_Init() 多次调用完成初始化,极大简化了复杂设备的固件架构设计。


2. 核心架构与数据流设计

2.1 分层模型与职责划分

ModbusSlaveRTU 采用清晰的四层架构:

层级 模块 主要职责 典型接口/依赖
物理层(PHY) 用户串口驱动 完成字节收发、波特率配置、RS-485 方向控制 void UART_Send(uint8_t *data, uint16_t len)
int32_t UART_Receive(uint8_t *buf, uint16_t len, uint32_t timeout_ms)
链路层(LINK) modbus_rtu_frame.c 帧定界(RTU 3.5 字符间隔检测)、CRC-16 校验、地址过滤 bool ModbusRTU_FrameParse(const uint8_t *raw, uint16_t len, ModbusFrame_t *out)
uint16_t ModbusRTU_CRC16(const uint8_t *data, uint16_t len)
协议层(PROTOCOL) modbus_slave.c 功能码解析(0x01/0x02/0x03/0x04/0x05/0x06/0x0F/0x10)、异常响应生成、从机地址匹配 ModbusStatus_t ModbusSlave_HandleRequest(const ModbusFrame_t *req, ModbusFrame_t *resp)
应用接口层(API) modbus_slave_rtu.h 提供初始化、接收/发送钩子注册、消息队列绑定、运行控制等统一入口 ModbusSlaveRTU_Handle_t ModbusSlaveRTU_Init(...)
void ModbusSlaveRTU_Run(ModbusSlaveRTU_Handle_t hnd)

⚠️ 关键设计说明: 链路层不持有任何全局状态 ModbusRTU_FrameParse() 是纯函数,输入原始字节流与长度,输出解析后的结构体; ModbusRTU_CRC16() 同样无副作用。这保证了多实例下各从机帧解析完全隔离,避免因静态变量引发的竞态问题。

2.2 消息队列机制详解

消息队列是 ModbusSlaveRTU 与上层业务解耦的核心枢纽。库定义了两个标准化消息结构:

// 请求消息:由库解析后投递至用户队列
typedef struct {
    uint8_t slave_addr;     // 从机地址(0x01~0xFF)
    uint8_t func_code;      // 功能码(0x01, 0x03, 0x10 等)
    uint16_t start_addr;    // 起始寄存器地址(0x0000~0xFFFF)
    uint16_t reg_count;     // 寄存器数量(读)或字节数(写)
    uint8_t *data_ptr;      // 指向有效载荷缓冲区(仅写操作)
    uint16_t data_len;      // 有效载荷长度(字节)
    void *user_ctx;         // 用户上下文指针(用于多实例区分)
} ModbusRequestMsg_t;

// 响应消息:由用户构造后提交至库发送队列
typedef struct {
    uint8_t slave_addr;
    uint8_t func_code;
    uint8_t *payload;       // 响应数据指针(含字节数字段)
    uint16_t payload_len;   // 响应数据长度(不含 CRC)
    uint32_t timeout_ms;    // 发送超时(默认 100ms)
} ModbusResponseMsg_t;

典型工作流程如下:

  1. 串口 ISR 接收完整帧 → 触发 ModbusSlaveRTU_OnFrameReceived(hnd, raw_buf, len)
  2. 链路层解析帧 → 若地址匹配且 CRC 正确 → 构造 ModbusRequestMsg_t
  3. 调用 xQueueSendToBack(req_queue, &req_msg, portMAX_DELAY) 投递至用户队列
  4. 用户任务 xQueueReceive(req_queue, &req, portMAX_DELAY) 取出请求
  5. 执行业务逻辑(如:读取 ADC 值存入 holding_reg[req.start_addr]
  6. 构造 ModbusResponseMsg_t 并调用 ModbusSlaveRTU_SendResponse(hnd, &resp)

此机制天然适配 FreeRTOS、Zephyr、RT-Thread 等主流 RTOS,亦可通过裸机环形缓冲区模拟队列行为,确保跨平台一致性。


3. 关键 API 接口与参数解析

3.1 初始化与配置接口

typedef struct {
    uint8_t slave_address;          // 本从机地址(必须唯一)
    uint32_t baudrate;              // 串口波特率(如 9600, 19200)
    uint8_t parity;                 // 校验位(MODBUS_PARITY_NONE/EVEN/ODD)
    uint8_t stop_bits;              // 停止位(MODBUS_STOP_BITS_1/2)
    uint16_t inter_frame_delay_us;  // RTU 帧间最小间隔(μs),默认 1750μs(3.5 字符)
    uint16_t response_timeout_ms;   // 响应超时(毫秒),默认 100ms
    QueueHandle_t req_queue;        // 请求消息队列句柄(FreeRTOS)
    QueueHandle_t resp_queue;       // 响应消息队列句柄(FreeRTOS)
    void (*uart_send)(const uint8_t*, uint16_t);  // 发送回调
    int32_t (*uart_recv)(uint8_t*, uint16_t, uint32_t); // 接收回调
    void *user_context;             // 用户私有数据(用于多实例区分)
} ModbusSlaveRTU_Config_t;

ModbusSlaveRTU_Handle_t ModbusSlaveRTU_Init(const ModbusSlaveRTU_Config_t *cfg);

参数关键点解析:

  • inter_frame_delay_us :RTU 协议强制要求帧间空闲时间 ≥ 3.5 字符周期。计算公式为 3.5 × (10 × 1000000) / baudrate (10 位/字符)。若配置过小,将导致帧粘连;过大则降低总线吞吐率。库内部使用 HAL_GetTick() k_uptime_get_32() 实现精确延时。
  • response_timeout_ms :指从接收到请求帧到开始发送响应帧的最大允许延迟。工业现场常要求 ≤ 50ms,该参数直接影响实时性。
  • req_queue / resp_queue :队列深度建议 ≥ 5,避免高负载下消息丢失。FreeRTOS 中需确保队列项大小 ≥ sizeof(ModbusRequestMsg_t)

3.2 运行时控制接口

// 主循环调用(裸机)或高优先级任务中调用
void ModbusSlaveRTU_Run(ModbusSlaveRTU_Handle_t hnd);

// 接收新帧通知(由串口 ISR 调用)
void ModbusSlaveRTU_OnFrameReceived(ModbusSlaveRTU_Handle_t hnd,
                                    const uint8_t *frame, uint16_t len);

// 发送响应(由用户任务调用)
ModbusStatus_t ModbusSlaveRTU_SendResponse(ModbusSlaveRTU_Handle_t hnd,
                                            const ModbusResponseMsg_t *resp);

// 获取当前统计信息(用于诊断)
typedef struct {
    uint32_t rx_frames;         // 接收总帧数
    uint32_t tx_frames;         // 发送总帧数
    uint32_t crc_errors;      // CRC 校验失败数
    uint32_t addr_mismatch;   // 地址不匹配数
    uint32_t illegal_func;    // 非法功能码数
    uint32_t illegal_data;    // 非法数据地址/值数
} ModbusSlaveStats_t;

void ModbusSlaveRTU_GetStats(ModbusSlaveRTU_Handle_t hnd, ModbusSlaveStats_t *stats);

ModbusSlaveRTU_Run() 是库的“心脏”,其内部执行以下原子操作:

  1. 检查接收队列是否有新帧(由 OnFrameReceived 触发)
  2. 解析帧并校验 CRC
  3. 若地址匹配,构造请求消息并投递至 req_queue
  4. 检查发送队列 resp_queue 是否有待发响应
  5. 调用 uart_send() 发送响应帧(自动追加 CRC)

该函数必须在确定性周期内调用(如 FreeRTOS 中 1ms tick 任务),确保协议栈及时响应。


4. 多实例实现原理与工程实践

4.1 多实例内存模型

ModbusSlaveRTU 采用 句柄(Handle)模式 管理多实例,每个实例对应一块独立的 ModbusSlaveRTU_Instance_t 结构体:

typedef struct {
    uint8_t address;
    uint16_t inter_frame_us;
    uint16_t resp_timeout_ms;
    QueueHandle_t req_q;
    QueueHandle_t resp_q;
    void *uart_ctx;           // 串口驱动私有上下文(如 UART_HandleTypeDef*)
    ModbusSlaveStats_t stats;
    // ... 其他运行时状态
} ModbusSlaveRTU_Instance_t;

// 全局实例数组(编译时确定最大实例数)
static ModbusSlaveRTU_Instance_t g_instances[MODBUS_SLAVE_MAX_INSTANCES];

ModbusSlaveRTU_Init() 返回指向该结构体的指针(即 Handle),所有后续 API 均以该 Handle 为第一参数,确保操作严格限定于指定实例。此设计避免了全局变量污染,符合 MISRA-C:2012 规则 8.8(外部对象声明需显式 extern)。

4.2 典型多实例部署示例(FreeRTOS)

// 定义两个从机实例
static QueueHandle_t g_holding_q, g_input_q;
static ModbusSlaveRTU_Handle_t g_holding_slave, g_input_slave;

void modbus_task(void *pvParameters) {
    // 创建请求队列(深度 10)
    g_holding_q = xQueueCreate(10, sizeof(ModbusRequestMsg_t));
    g_input_q   = xQueueCreate(10, sizeof(ModbusRequestMsg_t));

    // 初始化 Holding Register 从机(地址 0x01)
    ModbusSlaveRTU_Config_t cfg1 = {
        .slave_address = 0x01,
        .baudrate = 19200,
        .parity = MODBUS_PARITY_NONE,
        .stop_bits = MODBUS_STOP_BITS_1,
        .inter_frame_delay_us = 1750,
        .response_timeout_ms = 50,
        .req_queue = g_holding_q,
        .resp_queue = NULL, // 本例复用同一发送通道
        .uart_send = uart1_send_cb,
        .uart_recv = uart1_recv_cb,
        .user_context = (void*)0x01
    };
    g_holding_slave = ModbusSlaveRTU_Init(&cfg1);

    // 初始化 Input Register 从机(地址 0x02)
    ModbusSlaveRTU_Config_t cfg2 = {
        .slave_address = 0x02,
        .baudrate = 19200,
        .parity = MODBUS_PARITY_NONE,
        .stop_bits = MODBUS_STOP_BITS_1,
        .inter_frame_delay_us = 1750,
        .response_timeout_ms = 50,
        .req_queue = g_input_q,
        .resp_queue = NULL,
        .uart_send = uart1_send_cb,  // 复用同一 UART
        .uart_recv = uart1_recv_cb,
        .user_context = (void*)0x02
    };
    g_input_slave = ModbusSlaveRTU_Init(&cfg2);

    for(;;) {
        // 同时驱动两个实例
        ModbusSlaveRTU_Run(g_holding_slave);
        ModbusSlaveRTU_Run(g_input_slave);
        vTaskDelay(1); // 1ms 周期
    }
}

// 请求处理任务
void holding_handler_task(void *pvParameters) {
    ModbusRequestMsg_t req;
    for(;;) {
        if(xQueueReceive(g_holding_q, &req, portMAX_DELAY) == pdTRUE) {
            switch(req.func_code) {
                case 0x03: // 读保持寄存器
                    // 从硬件外设读取数据,填充 holding_reg[]
                    break;
                case 0x10: // 写多个保持寄存器
                    // 将 req.data_ptr 数据写入 holding_reg[req.start_addr]
                    break;
            }
            // 构造响应并发送
            ModbusResponseMsg_t resp = {
                .slave_addr = req.slave_addr,
                .func_code = req.func_code,
                .payload = g_resp_payload,
                .payload_len = calc_payload_len(req),
                .timeout_ms = 50
            };
            ModbusSlaveRTU_SendResponse(g_holding_slave, &resp);
        }
    }
}

✅ 工程优势:两个从机共享同一 UART 外设,但拥有完全独立的地址空间、错误计数器和业务逻辑,满足 IEC 61131-3 PLC 编程规范中“多逻辑设备”的要求。


5. 与主流 RTOS 的深度集成方案

5.1 FreeRTOS 集成要点

  • 中断安全 ModbusSlaveRTU_OnFrameReceived() 必须在中断服务程序(ISR)中调用,因此库内部所有队列操作均使用 xQueueSendToBackFromISR() ,避免在 ISR 中调用阻塞 API。
  • 内存分配 :库本身不调用 pvPortMalloc() ,所有内存由用户在初始化时静态分配( g_instances[] 数组),符合 ASIL-B 功能安全要求。
  • 优先级配置 :推荐将 modbus_task 设置为高于应用任务、低于定时器中断的优先级(如 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY - 1 ),确保协议栈及时响应。

5.2 Zephyr RTOS 集成示例

// Zephyr K_MSGQ 替代 FreeRTOS Queue
K_MSGQ_DEFINE(holding_msgq, sizeof(ModbusRequestMsg_t), 10, 4);
K_MSGQ_DEFINE(input_msgq, sizeof(ModbusRequestMsg_t), 10, 4);

// Zephyr 串口驱动回调
static void uart_callback(const struct device *dev, struct uart_event *evt, void *user_data) {
    if (evt->type == UART_RX_RDY) {
        ModbusSlaveRTU_OnFrameReceived(g_holding_slave, evt->data.rx.buf, evt->data.rx.len);
    }
}

// 初始化配置
struct modbus_slave_rtu_config zephyr_cfg = {
    .slave_address = 0x01,
    .req_msgq = &holding_msgq,
    .resp_msgq = NULL,
    .uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart1)),
    .uart_callback = uart_callback,
};

Zephyr 版本利用 K_MSGQ DEVICE_DT_GET 实现设备树驱动绑定,符合 Zephyr 的模块化设计理念。

5.3 裸机系统适配(无 RTOS)

对于 Cortex-M0+/M3 等资源极度受限 MCU,可采用环形缓冲区模拟队列:

#define REQ_BUF_SIZE 32
static ModbusRequestMsg_t g_req_buf[REQ_BUF_SIZE];
static volatile uint16_t g_req_head = 0, g_req_tail = 0;

// 伪队列发送(无锁,假设单生产者单消费者)
static bool ring_enqueue(const ModbusRequestMsg_t *msg) {
    uint16_t next = (g_req_head + 1) % REQ_BUF_SIZE;
    if (next != g_req_tail) {
        g_req_buf[g_req_head] = *msg;
        g_req_head = next;
        return true;
    }
    return false; // 队列满
}

// 在串口接收完成回调中调用
void uart_rx_complete_cb(uint8_t *buf, uint16_t len) {
    ModbusRequestMsg_t req = {...};
    if (ring_enqueue(&req)) {
        // 触发主循环处理标志
        g_modbus_req_pending = true;
    }
}

裸机版本牺牲了队列的阻塞特性,但代码体积 < 4KB,RAM 占用 < 512B,适用于 STM32F030 等超低成本 MCU。


6. 硬件驱动适配与 RS-485 控制

Modbus RTU 物理层通常基于 RS-485 总线,需精确控制 DE/RE 使能引脚。库提供 uart_send() 回调接口,用户需在此函数中完成方向切换:

// STM32 HAL 示例
static void uart1_send_cb(const uint8_t *data, uint16_t len) {
    // 1. 拉高 DE 引脚(发送使能)
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET);
    
    // 2. 等待 1-2 字符时间(确保总线稳定)
    HAL_Delay(1); // 或使用更精确的 us 延时
    
    // 3. 启动 DMA/IT 发送
    HAL_UART_Transmit_DMA(&huart1, (uint8_t*)data, len);
    
    // 4. 发送完成后,拉低 DE 引脚(接收使能)
    // 注:此部分需在 DMA TC 中断或 TXE 中断中执行
}

// 在 UART DMA 传输完成中断中
void USART1_IRQHandler(void) {
    HAL_UART_IRQHandler(&huart1);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);
    }
}

⚠️ 关键时序:DE 引脚必须在第一个字节起始位开始前至少 1 个比特时间置高,并在最后一个字节停止位结束后至少 1 个比特时间置低。库不介入硬件控制,将时序责任完全交由用户驱动,确保最高灵活性与可靠性。


7. 故障诊断与性能调优

7.1 常见问题定位表

现象 可能原因 检查方法 解决方案
从机无响应 inter_frame_delay_us 设置过小 用逻辑分析仪测量帧间空闲时间 按公式 3.5 × 10 × 1000000 / baudrate 重新计算
CRC 错误率高 串口时钟精度不足、线路干扰 检查 HAL_UART_GetState() 是否为 HAL_UART_STATE_READY 校准 HSI/HSI48,增加终端电阻,缩短线缆
请求丢失 req_queue 深度不足或 ModbusSlaveRTU_Run() 调用频率过低 调用 ModbusSlaveRTU_GetStats() 查看 rx_frames addr_mismatch 增大队列深度,提高 Run() 调用频率至 ≥ 1kHz
响应超时 response_timeout_ms 过小或业务逻辑耗时过长 测量从 xQueueReceive ModbusSlaveRTU_SendResponse 的耗时 优化业务代码,或将耗时操作移至低优先级任务

7.2 性能基准数据(STM32F407 @ 168MHz)

波特率 最大吞吐率(字节/秒) 平均响应延迟 CPU 占用率(1ms tick)
9600 850 12ms 0.8%
19200 1720 8ms 1.2%
38400 3450 6ms 1.9%
115200 10200 4ms 3.5%

数据表明:在 115200 波特率下,单实例 CPU 占用率仍低于 4%,为其他任务留出充足余量。若需更高吞吐,可启用 DMA 接收并配合双缓冲机制,进一步降低 CPU 开销。


8. 安全与可靠性增强实践

  • 地址空间保护 :库强制检查 start_addr + reg_count 不得越界,越界访问返回 0x02 (非法数据地址)异常,防止缓冲区溢出。
  • 功能码白名单 :默认仅启用 0x01/0x02/0x03/0x04/0x05/0x06/0x0F/0x10,禁用调试类功能码(如 0x08),符合 IEC 62443 安全要求。
  • CRC 硬件加速 :在 STM32F4/F7/H7 等 MCU 上,可将 ModbusRTU_CRC16() 替换为 HAL_CRC_Accumulate() ,提升 3 倍计算速度。
  • 看门狗协同 :在 ModbusSlaveRTU_Run() 开头喂狗,确保协议栈持续运行;若连续 5 秒未调用,则触发硬件复位。

某风电变流器项目实测:在 -40℃~85℃ 宽温环境中连续运行 18 个月, crc_errors illegal_func 均为 0,验证了其在严苛工业环境下的鲁棒性。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐