为什么需要服务层
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 服务层的线程安全来自三个层次:
- 邮箱:RT-Thread 邮箱是线程安全的 IPC 对象
- 信号量:每个请求有独立的完成信号量,不会交叉
- 单线程执行:所有事务在同一个线程里串行执行
不需要互斥锁——邮箱本身就起到了排队的作用。
小结
Modbus 服务层的设计哲学是:把共享资源的访问收敛到一个入口。
多个线程想用总线?排队。谁先到谁先用。执行完了通知你。业务层不需要知道 RS485 是半双工的,不需要知道 UART 怎么配置,只需要调用 Modbus_ServiceReadHoldingRegisters 就够了。