为什么需要服务层

RS485 是半双工总线——同一时刻只能有一个节点发送。如果采集线程和上行线程同时调用 Modbus_MasterReadHoldingRegisters,UART 发送会交错,从站收到乱帧,整个总线就挂了。

解决方案:所有 Modbus 请求都排队,由一个专用服务线程串行执行

邮箱 + 信号量模型

Modbus 服务层的核心是一个邮箱(mailbox)和每个请求自带的完成信号量:

调用方线程                    服务线程
    |                            |
    |--[请求指针]--> 邮箱 ------->|
    |                            |--[执行 Modbus 事务]
    |<--[done 信号量]-------------|
    |                            |
    |--[读取结果]                 |
typedef struct
{
    struct rt_semaphore done;      /* 完成信号量 */
    ModbusServiceTransaction transaction;
    rt_err_t status;               /* 事务执行结果 */
} ModbusServiceRequest;

调用方在栈上创建请求对象,投递指针到邮箱,然后阻塞等信号量。服务线程从邮箱取出指针,执行事务,释放信号量。调用方醒来读取结果。

服务线程主循环

static void Modbus_ServiceThreadEntry(void *parameter)
{
    g_modbus_service_ready = RT_TRUE;

    while (1)
    {
        rt_ubase_t value = 0U;
        ModbusServiceRequest *request;

        /* 阻塞等待请求 */
        if (rt_mb_recv(&g_modbus_service_mailbox, &value, RT_WAITING_FOREVER) != RT_EOK)
            continue;

        request = (ModbusServiceRequest *)value;

        /* 串行执行 */
        request->status = Modbus_ServiceProcessTransaction(&request->transaction);

        /* 通知调用方 */
        rt_sem_release(&request->done);
    }
}

邮箱保证了 FIFO 顺序,同一时刻只有一个事务在执行。

同步调用封装

业务层不需要关心邮箱和信号量,直接用封装好的同步函数:

rt_err_t Modbus_ServiceExecute(const ModbusServiceTransaction *transaction)
{
    ModbusServiceRequest request;
    memset(&request, 0, sizeof(request));
    request.transaction = *transaction;

    /* 创建私有信号量 */
    rt_sem_init(&request.done, "mb_ack", 0, RT_IPC_FLAG_PRIO);

    /* 投递到邮箱 */
    request.ipc_status = rt_mb_send_wait(&g_modbus_service_mailbox,
                                         (rt_ubase_t)&request,
                                         RT_WAITING_FOREVER);

    /* 阻塞等待完成 */
    rt_sem_take(&request.done, RT_WAITING_FOREVER);

    rt_sem_detach(&request.done);
    return request.status;
}

调用方的使用体验和直接调用底层函数一样,但背后是安全的串行化执行。

快捷封装

为了让业务代码更简洁,提供了几个常用操作的封装:

/* 读保持寄存器 */
rt_err_t Modbus_ServiceReadHoldingRegisters(slave_addr, start_addr,
                                            quantity, dest, timeout_ms);

/* 读输入寄存器 */
rt_err_t Modbus_ServiceReadRegisters(slave_addr, FC04, start_addr,
                                     quantity, dest, timeout_ms);

/* 写单个寄存器 */
rt_err_t Modbus_ServiceWriteSingleRegister(slave_addr, reg_addr,
                                           value, timeout_ms);

/* 写多个寄存器 */
rt_err_t Modbus_ServiceWriteMultipleRegisters(slave_addr, start_addr,
                                              quantity, src, timeout_ms);

这些函数内部都走 Modbus_ServiceExecute,保证串行化。

优先级设计

服务线程优先级设为 9,介于初始化线程(8)和采集线程(10)之间:

#define MODBUS_SERVICE_THREAD_PRIORITY      9U

这意味着:

  • 初始化阶段,服务线程不会抢占初始化线程
  • 采集线程发起请求后,服务线程能及时响应
  • 显示线程(15)和存储线程(18)不会干扰总线事务

邮箱深度

邮箱深度设为 8:

#define MODBUS_SERVICE_QUEUE_DEPTH          8U

正常情况下队列不会满——采集线程是串行的,每次请求等完成后才发下一个。只有在异常场景(比如多个线程同时写寄存器)才可能排队。

与 nanomodbus 的关系

Modbus 服务层不直接操作 UART,而是调用 Modbus_Master 层的函数:

static rt_err_t Modbus_ServiceProcessTransaction(const ModbusServiceTransaction *transaction)
{
    switch (transaction->operation)
    {
    case MODBUS_SERVICE_OP_READ_HOLDING:
        return Modbus_MasterReadHoldingRegisters(...);
    case MODBUS_SERVICE_OP_READ_INPUT:
        return Modbus_MasterReadRegisters(...);
    /* ... */
    }
}

Modbus_Master 内部用 nanomodbus 库处理 RTU 帧的组装/解析/CRC,以及 RS485 的 DE 引脚控制。

分层的好处:如果将来换 Modbus 库(比如换 libmodbus),只改 Modbus_Master 层,服务层和设备驱动都不受影响。

线程安全的保证

Modbus 服务层的线程安全来自三个层次:

  1. 邮箱:RT-Thread 邮箱是线程安全的 IPC 对象
  2. 信号量:每个请求有独立的完成信号量,不会交叉
  3. 单线程执行:所有事务在同一个线程里串行执行

不需要互斥锁——邮箱本身就起到了排队的作用。

小结

Modbus 服务层的设计哲学是:把共享资源的访问收敛到一个入口

多个线程想用总线?排队。谁先到谁先用。执行完了通知你。业务层不需要知道 RS485 是半双工的,不需要知道 UART 怎么配置,只需要调用 Modbus_ServiceReadHoldingRegisters 就够了。

后续阅读