nanoModbus 更适合放在嵌入式网关的协议边界
在 DALI 转 Modbus 网关里,nanoModbus 的职责是处理 Modbus RTU 从站协议,并把业务逻辑留给应用层。
项目硬件是 AIR001,DALI 侧负责照明总线的发送、接收和解析,RS485 侧对外暴露 Modbus RTU。从上位机看,它就是一组 holding register;从固件看,Modbus 写寄存器会触发 DALI 发送,DALI 接收结果会回填到寄存器里。

先把 Modbus 当成入口层
nanoModbus 只需要平台读写函数和回调表。串口、DMA、队列、业务状态都留在应用层,不塞进库里。
这个项目里,串口接收使用 DMA 环形缓冲和 idle 中断,再转存到一个字节队列。nanoModbus 的 read 回调只从这个队列里按超时时间取字节,write 回调只负责把响应发回 UART。
static int32_t nanoModbus_read_serial(uint8_t* buf,
uint16_t count,
int32_t byte_timeout_ms,
void* arg) {
uint32_t tick_start = HAL_GetTick();
uint16_t realReadCount = 0;
do {
uint8_t rx_data;
while (realReadCount < count &&
USART_read(&USART1_DMA_Context, &rx_data)) {
buf[realReadCount++] = rx_data;
}
} while (HAL_GetTick() - tick_start < byte_timeout_ms &&
realReadCount < count);
return realReadCount;
}
static int32_t nanoModbus_write_serial(const uint8_t* buf,
uint16_t count,
int32_t byte_timeout_ms,
void* arg) {
return USART_write(&USART1_DMA_Context, (uint8_t*)buf, count);
}这样做的好处是边界清楚:nanoModbus 不关心 UART 是轮询、中断还是 DMA;应用层也不需要解析 Modbus 帧、CRC 和异常响应。
寄存器表要表达业务动作
DALI 网关只开放 holding register。部分寄存器是状态,例如运行时间、接收长度、接收数据;部分寄存器是配置,例如控制器模式、是否监听自身发送;还有一个寄存器是动作触发:写入发送使能后,把前面两个消息字节发到 DALI 总线。
这个设计比另起一套私有命令更容易测试。Modbus Poll 直接读写寄存器,就能验证网关的主要行为。

寄存器地址不要散落在业务代码里。项目里用枚举固定寄存器顺序,读写回调统一检查范围和权限:
typedef enum
{
MODBUS_HOLDING_REG_IS_MILLIS_0 = 0,
MODBUS_HOLDING_REG_IS_MILLIS_1,
MODBUS_HOLDING_REG_IS_CONTROLER,
MODBUS_HOLDING_REG_TX_MSG_BOX_0,
MODBUS_HOLDING_REG_TX_MSG_BOX_1,
MODBUS_HOLDING_REG_TX_EN,
MODBUS_HOLDING_REG_RX_LEN,
MODBUS_HOLDING_REG_RX_MSG_BOX_0,
MODBUS_HOLDING_REG_RX_MSG_BOX_1,
MODBUS_HOLDING_REG_DEMO_SENDING_RESTRICTIONS,
MODBUS_HOLDING_REG_DEMO_IS_MONITOR_SELF_TX,
};读回调适合做轻量的状态整理。这个项目在读取时刷新运行时间,并在上位机读取接收消息后释放 IRQ 提示信号:
nmbs_error handler_read_holding_registers(uint16_t address,
uint16_t quantity,
uint16_t* registers_out,
uint8_t unit_id,
void* arg) {
if (address + quantity > HOLDING_REGISTERS_COUNT)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
if (address <= MODBUS_HOLDING_REG_RX_MSG_BOX_0 &&
address + quantity >= MODBUS_HOLDING_REG_RX_MSG_BOX_1) {
HAL_GPIO_WritePin(RX_IRQ_GPIO_Port, RX_IRQ_Pin, GPIO_PIN_SET);
}
*((uint32_t*)modbus_holding_registers) = HAL_GetTick();
memcpy(registers_out,
modbus_holding_registers + address,
sizeof(uint16_t) * quantity);
return NMBS_ERROR_NONE;
}写回调要区分“写状态”和“触发动作”。接收结果、演示限制计数这类寄存器由固件维护,上位机写入应该返回非法地址。发送使能寄存器不能简单保存值,写入时要读出消息寄存器并启动 DALI 发送。
nmbs_error handler_write_single_register(uint16_t address,
uint16_t value,
uint8_t unit_id,
void* arg) {
if (address >= HOLDING_REGISTERS_COUNT)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
switch (address) {
case MODBUS_HOLDING_REG_RX_LEN:
case MODBUS_HOLDING_REG_RX_MSG_BOX_0:
case MODBUS_HOLDING_REG_RX_MSG_BOX_1:
case MODBUS_HOLDING_REG_DEMO_SENDING_RESTRICTIONS:
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
}
switch (address) {
case MODBUS_HOLDING_REG_IS_CONTROLER:
dali_ctx.tx_isControler = value;
break;
case MODBUS_HOLDING_REG_DEMO_IS_MONITOR_SELF_TX:
dali_ctx.rx_isMonitorSelfTx = value;
break;
case MODBUS_HOLDING_REG_TX_EN: {
uint8_t sendData[2] = {
modbus_holding_registers[MODBUS_HOLDING_REG_TX_MSG_BOX_0] & 0xFF,
modbus_holding_registers[MODBUS_HOLDING_REG_TX_MSG_BOX_1] & 0xFF
};
DALI_start_tx_interrupt(&dali_ctx, sendData);
return NMBS_ERROR_NONE;
}
}
modbus_holding_registers[address] = value;
return NMBS_ERROR_NONE;
}触发型寄存器如果被当成普通状态保存,后面维护时很容易误判设备状态。例如 TX_EN 的意义是“这次写入触发发送”,不能表示“设备当前处于发送使能状态”。这类寄存器最好在写回调里立即消费掉。
DALI 侧只通过状态和事件接入
DALI 接收完成后,不需要直接参与 Modbus 协议处理。它只把解析结果写进 holding register,并拉低 IRQ 引脚通知外部设备有新数据。
void rx_interrupt_callback(uint8_t* data, uint8_t len) {
modbus_holding_registers[MODBUS_HOLDING_REG_RX_LEN] = len;
modbus_holding_registers[MODBUS_HOLDING_REG_RX_MSG_BOX_0] = data[0];
modbus_holding_registers[MODBUS_HOLDING_REG_RX_MSG_BOX_1] = data[1];
HAL_GPIO_WritePin(RX_IRQ_GPIO_Port, RX_IRQ_Pin, GPIO_PIN_RESET);
}这个边界让系统更容易排查。DALI 定时器、中断解析、曼彻斯特编码可以单独调;Modbus Poll 读写寄存器也可以单独调。两边通过一张小寄存器表连接。

主循环里不要空跑协议栈
工程里还有一个细节:只有串口队列不为空时才调用 nmbs_server_poll()。
if (!isQueueEmpty(&USART1_DMA_Context.queue) &&
nmbs_server_poll(&nmbs) == NMBS_ERROR_TRANSPORT) {
Error_Handler();
}
DALI_Update(&dali_ctx);这样做可以避免主循环长期卡在无意义的 Modbus 读取等待里。DALI 侧还需要持续处理接收超时和帧解析,协议入口不能抢走整个主循环。
超时也要按串口接收方式理解。byte_timeout_ms 是 nanoModbus 等待下一个字节的边界,不代表 DALI 业务超时。DMA idle 中断已经负责把收到的数据搬到队列里,read 回调只要在这个边界内尽量取满即可。
这类项目的移植结论
nanoModbus 适合承担协议边界,不适合承担业务模型。嵌入式网关里最稳的用法是:
- 串口收发封装成平台
read和write。 - 业务能力整理成 holding register。
- 只读寄存器在写回调里明确拒绝。
- 触发型寄存器在写回调里立即消费。
- 异步总线事件只更新寄存器和通知引脚。
- 主循环避免因为 Modbus 等待阻塞其他实时任务。
这样移植后,nanoModbus 负责协议正确性,应用代码负责设备行为。两边边界清楚,后续换串口驱动、扩寄存器、接入新的现场总线时都更容易维护。
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。