目录
- 一、前言
- [二、设计思路:共享寄存器 + 互斥锁](#二、设计思路:共享寄存器 + 互斥锁)
- [三、modbus_mapping_t 结构体](#三、modbus_mapping_t 结构体)
- [四、TCP Server 任务:初始化与调度](#四、TCP Server 任务:初始化与调度)
- [五、RTU Server 任务:复用资源](#五、RTU Server 任务:复用资源)
- 六、两个任务的协作关系
- 七、总结
- 八、结尾
一、前言
大家好,这里是 Hello_Embed 。上篇对 H5 开发板程序进行了六处针对性改进。本篇来实现最终目标:让中控同时支持 Modbus RTU 和 Modbus TCP 两种协议。
整体效果:上位机既可以通过 USB 串口(RTU) 与中控通信,也可以通过 WiFi 网络(TCP) 访问,两种协议读写的是同一套寄存器 ,并通过 LVGL 界面实现协议切换。配合写文件功能,还可以进行远程 IAP 升级。
改造完全基于原 RTU 工程,只展示关键改动部分。
二、设计思路:共享寄存器 + 互斥锁
双协议兼容的核心,只需解决一个问题:
如何让两种协议安全地访问同一套寄存器,而不发生冲突?
答案是:用一个全局互斥锁(Mutex)保护寄存器访问。
┌─────────────────────────────────────────┐
│ 共享寄存器 mb_mapping │
└──────────────────┬──────────────────────┘
│ 受全局 Mutex 保护
┌───────────┴───────────┐
↓ ↓
LibmodbusTCPServerTask LibmodbusRTUServerTask
(WiFi / TCP 通道) (USB / RTU 通道)
两个任务在处理上位机请求时,都要先获取互斥锁 ,操作完寄存器后再释放锁。这样无论哪个任务在运行,寄存器数据始终是一致的。
数据格式的适配(RTU/TCP 帧头差异)在前几篇中已经完成,本篇专注于任务间的资源共享与互斥。
三、modbus_mapping_t 结构体
两个任务共同访问的寄存器映射表,其结构如下:
c
typedef struct _modbus_mapping_t {
int nb_bits; /* 线圈数量 */
int start_bits; /* 线圈起始地址 */
int nb_input_bits; /* 离散输入数量 */
int start_input_bits;
int nb_input_registers; /* 输入寄存器数量 */
int start_input_registers;
int nb_registers; /* 保持寄存器数量 */
int start_registers;
uint8_t *tab_bits; /* 线圈数据 */
uint8_t *tab_input_bits; /* 离散输入数据 */
uint16_t *tab_input_registers;/* 输入寄存器数据 */
uint16_t *tab_registers; /* 保持寄存器数据 */
} modbus_mapping_t;
解析 :
mb_mapping是整个 Modbus 数据模型的核心------上位机读写的线圈、寄存器,本质上都是读写这个结构体里的数组。两个协议任务共享同一个mb_mapping实例,对同一套物理数据进行操作,从而实现"RTU 写入、TCP 可读"的双向透明。
四、TCP Server 任务:初始化与调度
TCP Server 任务承担了所有全局资源的初始化工作,然后再创建 RTU 任务:
c
void LibmodbusTCPServerTask(void *pvParameters)
{
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
PChannelInfo ptChannelInfo;
int channel;
int err;
int header_length;
int socket_server, socket_client;
/* 显示当前运行模式 */
if (isBootloader())
Draw_String(150, 0, "Bootloader", 0xff0000, 0);
else
Draw_String(150, 0, "Application", 0xff0000, 0);
/* ① 创建共享寄存器映射表,供两个任务共用 */
mb_mapping = modbus_mapping_new_start_address(0, 500, 0, 500, 0, 500, 0, 500);
memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
memset(mb_mapping->tab_registers, 0, mb_mapping->nb_registers * 2);
g_mb_mapping = mb_mapping; /* 存入全局变量,供 RTU 任务使用 */
/* ② 创建全局互斥锁 */
g_mb_lock = xSemaphoreCreateMutex();
/* ③ 初始化通道映射信息 */
for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
{
ptChannelInfo = &g_tChannelInfos[channel];
ptChannelInfo->mb_mapping = mb_mapping;
ptChannelInfo->xMutex = xSemaphoreCreateMutex();
}
/* ④ 创建业务任务(Bootloader 模式下不需要) */
if (!isBootloader())
{
xTaskCreate(CH0_Task, "CH0_Task", 400, mb_mapping, osPriorityNormal, NULL);
for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
{
ptChannelInfo = &g_tChannelInfos[channel];
xTaskCreate(CHn_Task, ptChannelInfo->uart_name, 400,
ptChannelInfo, osPriorityNormal, NULL);
}
}
/* ⑤ 创建 RTU Server 任务(在 TCP 初始化之后,确保 g_mb_mapping 已就绪) */
xTaskCreate(LibmodbusRTUServerTask, "LibmodbusRTUServerTask",
400, NULL, osPriorityNormal, NULL);
/* ⑥ 连接 WiFi 热点 */
at_init("uart1");
while (1)
{
err = at_connect_ap("Programmers", "100asktech");
if (!err)
break;
vTaskDelay(1000);
}
/* ⑦ 创建 TCP 上下文并监听 */
ctx = modbus_new_tcp(NULL, 1502);
query = pvPortMalloc(MODBUS_TCP_MAX_ADU_LENGTH);
header_length = modbus_get_header_length(ctx);
Draw_String(0, 48, "Waiting connect ...", 0xff0000, 0);
while (1)
{
socket_server = modbus_tcp_listen(ctx, 1);
if (socket_server >= 0)
break;
}
while (1)
{
socket_client = modbus_tcp_accept(ctx, &socket_server);
if (socket_client >= 0)
break;
}
Draw_String(0, 64, "Modbus client connected", 0xff0000, 0);
/* ⑧ 主循环:接收 → 加锁 → 处理 → 回复 → 解锁 */
for (;;)
{
do {
rc = modbus_receive(ctx, query);
} while (rc == 0);
if (rc < 0)
{
/* Socket 出错,等待重连 */
Draw_String(0, 80, "wait re-connect ...", 0xff0000, 0);
while (1)
{
socket_client = modbus_tcp_accept(ctx, &socket_server);
if (socket_client >= 0)
break;
}
Draw_String(0, 96, "Modbus client re-connected", 0xff0000, 0);
continue;
}
/* 获取互斥锁,保护寄存器访问 */
xSemaphoreTake(g_mb_lock, portMAX_DELAY);
err = process_emergency_cmd(ctx, query, rc, mb_mapping);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
xSemaphoreGive(g_mb_lock);
continue;
}
err = process_file_record(&query[header_length - 1], rc);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
xSemaphoreGive(g_mb_lock);
continue;
}
rc = modbus_reply(ctx, query, rc, mb_mapping);
/* 释放互斥锁 */
xSemaphoreGive(g_mb_lock);
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
解析 :初始化顺序很关键------
g_mb_mapping和g_mb_lock必须在创建 RTU 任务之前就准备好,否则 RTU 任务启动后访问全局变量会拿到空指针。因此 TCP 任务先做所有初始化,再拉起 RTU 任务,最后自己才去连 WiFi 和监听。
五、RTU Server 任务:复用资源
RTU 任务无需重复初始化,直接从全局变量取 mb_mapping 和互斥锁:
c
void LibmodbusRTUServerTask(void *pvParameters)
{
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
int err;
int header_length;
/* RTU 上下文:通过 USB 串口通信 */
ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);
modbus_set_slave(ctx, 1);
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
/* 直接复用 TCP 任务创建的共享寄存器映射表 */
mb_mapping = g_mb_mapping;
header_length = modbus_get_header_length(ctx);
rc = modbus_connect(ctx);
if (rc == -1)
{
modbus_free(ctx);
vTaskDelete(NULL);
}
/* 主循环:与 TCP 任务结构完全对称 */
for (;;)
{
do {
rc = modbus_receive(ctx, query);
} while (rc == 0);
if (rc < 0)
{
/* RTU 出错直接跳过,等待下一帧 */
continue;
}
/* 获取互斥锁,保护寄存器访问 */
xSemaphoreTake(g_mb_lock, portMAX_DELAY);
err = process_emergency_cmd(ctx, query, rc, mb_mapping);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
xSemaphoreGive(g_mb_lock);
continue;
}
err = process_file_record(&query[header_length - 1], rc);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
xSemaphoreGive(g_mb_lock);
continue;
}
rc = modbus_reply(ctx, query, rc, mb_mapping);
/* 释放互斥锁 */
xSemaphoreGive(g_mb_lock);
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
解析 :RTU 任务的逻辑与 TCP 任务完全对称 ------都是"收包 → 加锁 → 处理 → 回复 → 解锁"的循环。区别只在于底层的通信接口(RTU 用 USB 串口,TCP 用 WiFi)。RTU 出错时不需要重连,直接
continue等下一帧即可,因为串口连接是持久的。
六、两个任务的协作关系
| 维度 | TCP 任务 | RTU 任务 |
|---|---|---|
| 通信接口 | WiFi / W800 模块 | USB 串口 |
| 寄存器来源 | 创建并初始化 g_mb_mapping |
直接使用 g_mb_mapping |
| 互斥锁 | 创建 g_mb_lock |
直接使用 g_mb_lock |
| 出错处理 | modbus_tcp_accept 等待重连 |
continue 跳过,等待下一帧 |
| 任务创建顺序 | 先启动,负责全局初始化 | 由 TCP 任务拉起 |
两个任务通过全局互斥锁 g_mb_lock 实现对 mb_mapping 的互斥访问,任何时刻只有一个任务能持有锁、操作寄存器,从而保证数据一致性。
七、总结
- 双协议兼容的核心 :共享
mb_mapping,用全局互斥锁保护访问 - 初始化顺序:TCP 任务负责创建所有共享资源,再拉起 RTU 任务,避免竞态
- 对称的任务结构:两个任务主循环逻辑完全一致,仅底层通信接口不同
- 数据格式适配 :RTU/TCP 帧头差异由
header_length动态处理,上层逻辑无感知
经过本篇改造,系统已经能够:
- 上位机通过 USB 串口(RTU) 读写寄存器
- 上位机通过 WiFi(TCP) 读写同一套寄存器
- 结合写文件功能,支持远程 IAP 升级
八、结尾
本篇完成了 RTU/TCP 双协议并行的核心设计,至此整个工业物联网设备管理系统的通信层已经全部打通。
下一篇将学习 MQTT 协议,并尝试将相关代码移植到 FreeRTOS,敬请期待~
Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~