嵌入式上位机开发入门(二十二):RTU/TCP 双协议互斥访问寄存器

目录

  • 一、前言
  • [二、设计思路:共享寄存器 + 互斥锁](#二、设计思路:共享寄存器 + 互斥锁)
  • [三、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_mappingg_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 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
南宫萧幕2 小时前
自动控制原理|稳定性与劳斯判据 知识点+计算题+MATLAB实现全套笔记
笔记·matlab·控制
.select.2 小时前
TCP 8 (拥塞控制)
服务器·网络·tcp/ip
有个人神神叨叨2 小时前
Claude Managed Agents 快速入门笔记
人工智能·笔记
yuyuyuliang003 小时前
python笔记1
开发语言·笔记·python
lonelyhiker3 小时前
cas学习笔记
数据库·笔记·学习
不想学习\??!3 小时前
USB-HID学习笔记
笔记·学习
码农的小菜园4 小时前
提示工程学习笔记(一)
笔记·学习
四谎真好看4 小时前
Redis学习笔记(高级篇3)
redis·笔记·学习·学习笔记