第十一章:主控访问多个传感器(Modbus 网关/桥接器设计)

11.1 项目背景与硬件连接

在实际工业应用中,常常需要通过一个主控设备(如STM32H5)同时管理多个Modbus从设备(传感器/执行器),并将这些设备的数据汇总,提供给上位机(PC)统一访问。本章以三个传感器为例,演示如何通过一个H5主控板实现多传感器访问。

11.1.1 硬件连接图

根据提供的示意图,硬件连接如下:

cs 复制代码
text

PC (USB串口)
  │
  └── H5主控板 (STM32H5)
        ├── CH1 (UART2) ── 485总线 ── 开关量传感器 (地址1)
        │                      └── 环境监测传感器 (地址2)
        └── CH2 (UART4) ── 485总线 ── 温湿度传感器 (地址3)
  • H5主控板通过USB串口与PC连接,PC作为Modbus主站。

  • H5主控板有两路RS485接口(CH1和CH2),分别挂载不同的传感器。

  • 三个传感器的地址分别设为1、2、3,且启动开关拨到"ON"位置。

11.1.2 为什么需要主控?
  • PC只有一个串口,无法直接并联多个485设备(地址可区分,但若使用同一总线则所有设备共享,这里CH1挂了两个设备,但使用不同地址,是可行的;CH2单独挂一个)。实际上PC可以直接访问每个传感器,但本章的目的是演示H5作为"网关":PC只需与H5通信,H5负责与各个传感器交互,这样PC只需知道H5的协议,无需关心底层传感器细节。

  • 更常见的场景:H5采集多个传感器的数据,进行本地处理(如显示、控制逻辑),再统一上报给上位机。

11.2 整体软件架构

H5主控板运行FreeRTOS,创建多个任务协同工作:

cs 复制代码
text

任务划分:
├── 任务1:Modbus Server(从站)任务(处理PC请求)
│    使用UART(与PC相连),响应PC的Modbus请求,读写全局映射表。
├── 任务2:CH1 Client(主站)任务(访问开关量传感器,地址1)
│    通过UART2发送Modbus请求,读取按键状态,写入继电器/LED控制。
├── 任务3:CH1 Client(主站)任务(访问环境监测传感器,地址2)
│    通过UART2发送Modbus请求,读取ADC值,写入蜂鸣器/LED控制。
└── 任务4:CH2 Client(主站)任务(访问温湿度传感器,地址3)
    通过UART4发送Modbus请求,读取温湿度,写入蜂鸣器/LED控制。

任务2、3、4周期性地与对应传感器通信,将读取到的数据更新到全局Modbus映射表中,同时将映射表中来自PC的写请求(如控制输出)发送给传感器。

任务1则直接使用这个全局映射表回复PC的请求,实现PC与传感器数据的桥接。

11.3 三个传感器的寄存器点表回顾

为了后续理解映射,先回顾每个传感器的寄存器定义。

11.3.1 开关量传感器(地址1)
寄存器地址 类别 用途 描述
0000H DI 读取KEY1 1=按下
0001H DI 读取KEY2 1=按下
0002H DI 读取KEY3 1=按下
0000H DO 控制继电器1 1=吸合
0001H DO 控制继电器2 1=吸合
0002H DO 控制LED1 1=亮
0003H DO 控制LED2 1=亮
0004H DO 控制LED3 1=亮
11.3.2 环境监测传感器(地址2)
寄存器地址 类别 用途 描述
0000H DO 控制蜂鸣器1 1=响
0001H DO 控制蜂鸣器2 1=响
0002H DO 控制LED1 1=亮
0003H DO 控制LED2 1=亮
0004H DO 控制LED3 1=亮
0000H AI 光敏电压ADC值 0xfff对应3.3V,12位精度
0001H AI 可调电阻ADC值 0xfff对应3.3V,12位精度
11.3.3 温湿度传感器(地址3)
寄存器地址 类别 用途 描述
0000H DO 控制蜂鸣器1 1=响
0001H DO 控制蜂鸣器2 1=响
0002H DO 控制LED1 1=亮
0003H DO 控制LED2 1=亮
0004H DO 控制LED3 1=亮
0000H AI 读取温度 单位0.1°C,有符号16位
0001H AI 读取湿度 单位0.1%RH,无符号16位

11.4 H5主控的全局映射表设计

H5主控需要将三个传感器的数据整合到一个统一的Modbus地址空间中,供PC访问。根据提供的表格,映射关系如下:

11.4.1 离散输入(DI)映射
H5地址 类别 来源 说明
0000H DI 开关量传感器(ID1)DI0 KEY1状态
0001H DI 开关量传感器(ID1)DI1 KEY2状态
0002H DI 开关量传感器(ID1)DI2 KEY3状态
11.4.2 线圈(DO)映射
H5地址 类别 目标 说明
0000H DO H5自身LED 控制H5板上的LED(可选)
0001H DO 开关量传感器(ID1)DO0 继电器1
0002H DO 开关量传感器(ID1)DO1 继电器2
0003H DO 开关量传感器(ID1)DO2 LED1
0004H DO 开关量传感器(ID1)DO3 LED2
0005H DO 开关量传感器(ID1)DO4 LED3
0006H DO 环境监测传感器(ID2)DO0 蜂鸣器1
0007H DO 环境监测传感器(ID2)DO1 蜂鸣器2
0008H DO 环境监测传感器(ID2)DO2 LED1
0009H DO 环境监测传感器(ID2)DO3 LED2
000AH DO 环境监测传感器(ID2)DO4 LED3
000BH DO 温湿度传感器(ID3)DO0 蜂鸣器1
000CH DO 温湿度传感器(ID3)DO1 蜂鸣器2
000DH DO 温湿度传感器(ID3)DO2 LED1
000EH DO 温湿度传感器(ID3)DO3 LED2
000FH DO 温湿度传感器(ID3)DO4 LED3
11.4.3 输入寄存器(AI)映射
H5地址 类别 来源 说明
0000H AI 环境监测传感器(ID2)AI0 光敏电压ADC值
0001H AI 环境监测传感器(ID2)AI1 可调电阻ADC值
0002H AI 温湿度传感器(ID3)AI0 温度值(0.1°C)
0003H AI 温湿度传感器(ID3)AI1 湿度值(0.1%RH)

注意:H5主控本身也有LED,可以控制,但这里映射表中包含H5自身LED(地址0000H)。开关量传感器没有AI,环境监测没有DI,等等。

11.5 H5主控任务详细设计

11.5.1 全局共享数据

使用一个modbus_mapping_t结构体作为全局映射,被所有任务访问。该映射根据上述表格定义数量和起始地址。

cs 复制代码
// 假设映射起始地址均为0,数量如下:
#define H5_NB_BITS            16   // 线圈数量(0~15)
#define H5_NB_INPUT_BITS      3    // 离散输入数量(0~2)
#define H5_NB_INPUT_REGISTERS 4    // 输入寄存器数量(0~3)
#define H5_NB_REGISTERS       0    // 保持寄存器未使用

modbus_mapping_t *g_h5_mapping;

在初始化时创建:

cs 复制代码
g_h5_mapping = modbus_mapping_new_start_address(
    0, H5_NB_BITS,
    0, H5_NB_INPUT_BITS,
    0, H5_NB_INPUT_REGISTERS,
    0, H5_NB_REGISTERS
);
11.5.2 任务1:Modbus Server(响应PC)
  • 使用与PC相连的串口(如UART1)创建Modbus RTU上下文,从站地址设为1(H5自身的地址)。

  • 在循环中调用modbus_receive接收PC请求,然后调用modbus_reply,直接使用全局映射g_h5_mapping回复。

  • 因为映射中的数据由其他任务更新,无需在此任务中额外处理。

11.5.3 任务2:访问开关量传感器(CH1,ID1)
  • 使用UART2创建Modbus RTU上下文。

  • 循环中:

    • 读取传感器离散输入(功能码02):读取地址0~2,存入g_h5_mapping->tab_input_bits[0~2]

    • g_h5_mapping->tab_bits中取出对应线圈的值(地址1~5),写入传感器(功能码05或15)。

    • 可选:同时读取传感器线圈状态(但此处未做,因为输出由主控决定,无需读回)。

注意:操作前需用modbus_set_slave(ctx, 1)设置目标从站地址为1。

代码片段:

cs 复制代码
c

static void LibmodbusCH1ClientTask(void *pvParameters)
{
    modbus_t *ctx;
    uint8_t bits[3];
    int led_status = 1;  // 用于控制H5自身LED的变量

    ctx = modbus_new_rtu("uart2", 115200, 'N', 8, 1);
    modbus_connect(ctx);

    while (1) {
        // 读取开关量传感器的按键
        modbus_set_slave(ctx, 1);
        modbus_read_input_bits(ctx, 0, 3, bits);
        g_h5_mapping->tab_input_bits[0] = bits[0];
        g_h5_mapping->tab_input_bits[1] = bits[1];
        g_h5_mapping->tab_input_bits[2] = bits[2];

        // 将H5映射中的线圈值写入传感器(地址1~5对应H5地址1~5)
        // 注意:H5地址0是H5自身LED,这里不写
        for (int i = 1; i <= 5; i++) {
            int val = (g_h5_mapping->tab_bits[i] ? 1 : 0);
            modbus_write_bit(ctx, i-1, val);  // 传感器DO地址从0开始
        }

        // 控制H5自身LED(H5地址0)
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, g_h5_mapping->tab_bits[0] ? GPIO_PIN_SET : GPIO_PIN_RESET);

        vTaskDelay(100);
    }
}
11.5.4 任务3:访问环境监测传感器(CH1,ID2)
  • 同样使用UART2,但目标从站地址为2。

  • 读取两个输入寄存器(光敏和可调电阻),存入g_h5_mapping->tab_input_registers[0~1]

  • 写入线圈:H5地址6~10对应此传感器的DO0~4。

代码类似,略。

11.5.5 任务4:访问温湿度传感器(CH2,ID3)
  • 使用UART4,目标从站地址3。

  • 读取两个输入寄存器(温度、湿度),存入g_h5_mapping->tab_input_registers[2~3]

  • 写入线圈:H5地址11~15对应此传感器的DO0~4。

11.5.6 LCD显示与互斥

所有任务都可能需要更新LCD显示(显示传感器数据)。LCD驱动通常不支持并发访问,因此需要使用互斥信号量保护。

代码示例:

cs 复制代码
c

static SemaphoreHandle_t g_spi_lcd_lock;

void Draw_Init(void)
{
    g_spi_lcd_lock = xSemaphoreCreateMutex();
}

void Draw_String(uint32_t x, uint32_t y, char *str, uint32_t color, uint32_t bg_color)
{
    xSemaphoreTake(g_spi_lcd_lock, portMAX_DELAY);
    // 实际的LCD画字符串函数
    // ...
    xSemaphoreGive(g_spi_lcd_lock);
}

在任务2、3、4中读取数据后,使用Draw_String在LCD不同行显示信息。

11.6 关键代码解析

11.6.1 任务2代码逐行解释
cs 复制代码
c

static void LibmodbusCH1ClientTask( void *pvParameters )	
{
    modbus_t *ctx;
    int rc;
    uint16_t vals[10];
    int nb = 1;
    uint8_t bits[10];
    uint8_t buf[100];
    int led_status = 1;
    
    // 创建Modbus RTU上下文,使用UART2,波特率115200,无校验,8数据位,1停止位
    ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);
    modbus_set_slave(ctx, 1);  // 设置默认从站地址为1(开关量传感器)
    
    rc = modbus_connect(ctx);
    if (rc == -1) {
        modbus_free(ctx);
        vTaskDelete(NULL);
    }

    for (;;) {
        /* 读取开关量传感器(ID=1)的按键状态(离散输入) */
        modbus_set_slave(ctx, 1);  // 确保从站地址为1
        rc = modbus_read_input_bits(ctx, 0, 3, bits);
        if (rc == 3)
        {
            // 更新全局映射的离散输入位
            g_h5_mapping->tab_input_bits[0] = bits[0];
            g_h5_mapping->tab_input_bits[1] = bits[1];
            g_h5_mapping->tab_input_bits[2] = bits[2];
            
            // 显示在LCD上
            sprintf(buf, "SWITCH keys: %d %d %d", bits[0], bits[1], bits[2]);
            Draw_String(0, 0, buf, 0xff0000, 0);
        }

        // 向传感器写入线圈(控制继电器/LED),从H5映射的线圈地址1~5对应传感器DO0~4
        rc = modbus_write_bit(ctx, 2, led_status);  // 示例:控制传感器LED1(地址2)闪烁
        
        /* 读取环境监测传感器(ID=2)的ADC值 */
        modbus_set_slave(ctx, 2);
        rc = modbus_read_input_registers(ctx, 0, 2, vals);
        if (rc == 2)
        {
            g_h5_mapping->tab_input_registers[0] = vals[0];
            g_h5_mapping->tab_input_registers[1] = vals[1];
            sprintf(buf, "ENV Sensor : opti 0x%x, res 0x%x          ", vals[0], vals[1]);
            Draw_String(0, 16, buf, 0xff0000, 0);
        }

        // 同样对环境传感器写入线圈(如控制蜂鸣器)
        rc = modbus_write_bit(ctx, 2, led_status);  // 示例:控制环境传感器的LED1(地址2)

        led_status = !led_status;  // 翻转,用于闪烁

        vTaskDelay(500);  // 每500ms执行一次
    }

    modbus_close(ctx);
    modbus_free(ctx);
    vTaskDelete(NULL);
}

注意 :此任务同时访问了两个传感器(ID1和ID2),因为它们在同一个485总线上(CH1)。每次访问前都通过modbus_set_slave切换从站地址。

11.6.2 任务4代码(温湿度传感器)
cs 复制代码
c

static void LibmodbusCH2ClientTask( void *pvParameters )	
{
    modbus_t *ctx;
    int rc;
    uint16_t vals[10];
    uint8_t buf[100];
    int led_status = 1;
    
    ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);
    modbus_set_slave(ctx, 3);  // 默认从站地址3
    
    rc = modbus_connect(ctx);
    if (rc == -1) {
        modbus_free(ctx);
        vTaskDelete(NULL);
    }

    for (;;) {
        /* 读取温湿度(输入寄存器) */
        rc = modbus_read_input_registers(ctx, 0, 2, vals);
        if (rc == 2)
        {
            g_h5_mapping->tab_input_registers[2] = vals[0];
            g_h5_mapping->tab_input_registers[3] = vals[1];
            // 显示温度:整数部分=vals[0]/10,小数部分=vals[0]%10
            sprintf(buf, "TEM/HUM Sensor : temp %d.%d, humi %d.%d          ",
                    vals[0]/10, vals[0]%10, vals[1]/10, vals[1]%10);
            Draw_String(0, 32, buf, 0xff0000, 0);
        }

        // 向传感器写入线圈(控制蜂鸣器/LED)
        rc = modbus_write_bit(ctx, 2, led_status);  // 示例:控制LED1闪烁

        led_status = !led_status;
        vTaskDelay(500);
    }

    modbus_close(ctx);
    modbus_free(ctx);
    vTaskDelete(NULL);
}
11.6.3 LCD互斥实现
cs 复制代码
c

void Draw_Init(void)
{
    g_spi_lcd_lock = xSemaphoreCreateMutex();
}

static void Draw_Region(uint32_t x, uint32_t y, P_BitMap ptBitMap)
{   
    xSemaphoreTake(g_spi_lcd_lock, portMAX_DELAY);
    LCD_SetWindows(x, y, x + ptBitMap->width - 1, y + ptBitMap->height - 1);
    LCD_SetDataLine();
    LCD_WriteDatas(ptBitMap->datas, ptBitMap->height * ptBitMap->width * 2);
    xSemaphoreGive(g_spi_lcd_lock);
}

Draw_String内部会调用类似Draw_Region,确保多任务下LCD显示不会错乱。

11.7 主控作为Modbus网关的原理

  • 上行(PC -> H5):H5作为Modbus从站,PC通过标准Modbus协议读写H5的寄存器(线圈、离散输入、输入寄存器)。H5的任务1直接使用全局映射回复,无需关心底层传感器。

  • 下行(H5 -> 传感器):H5作为Modbus主站,通过任务2、3、4周期性地与各个传感器通信,读取输入数据更新映射,并将映射中的输出数据写入传感器。

这样,PC看到的是一个统一的设备(H5),而实际背后有多个传感器。这种模式在工业中称为协议转换器网关

11.8 总结与注意事项

  1. 地址映射是关键:必须精心设计H5的寄存器地址与各个传感器寄存器的对应关系,避免冲突。

  2. 多任务同步:全局映射被多个任务读写,需要确保数据一致性。本例中,读和写是分离的,且任务间没有同时写同一位置(不同任务写不同区域),因此暂无需互斥。但如果存在多个任务写同一寄存器,则需加锁。

  3. LCD互斥:任何使用LCD的任务必须获取互斥信号量,防止显示混乱。

  4. 从站地址管理:H5自身地址为1,三个传感器地址分别为1、2、3,注意传感器地址1与H5自身地址相同,但不在同一总线上(传感器1在CH1,H5自身在PC串口),所以不冲突。但如果同一总线上有两个相同地址的设备就会冲突,这里CH1上的两个传感器地址不同(1和2),没问题。

  5. 错误处理:代码中简化为如果连接失败则删除任务,实际应增加重连机制。

  6. 扩展性:可以轻松添加更多传感器,只需增加任务和映射表项。

通过本章学习,你掌握了如何设计一个Modbus网关,将多个从设备的数据汇集到一个主控,再统一对外提供服务。这是工业物联网中非常常见的应用模式。

相关推荐
~央千澈~1 小时前
抖音弹幕游戏开发之第10集:整合 - 弹幕触发键盘操作·优雅草云桧·卓伊凡
开发语言·python·计算机外设
Laughtin2 小时前
macos的python安装选择以及homebrew python的安装方法
开发语言·python·macos
默凉2 小时前
C++ 编译过程
开发语言·c++
csbysj20202 小时前
C 标准库 - `<errno.h>`
开发语言
Highcharts.js2 小时前
Highcharts 3D漏斗图(Funnel 3D)完全指南:从模块加载到一文学会三维漏斗可视化
javascript·开发文档·highcharts·图表开发·漏斗图·3d 图表
人道领域2 小时前
Maven多模块开发:高效构建复杂项目
java·开发语言·spring boot·maven
FunW1n2 小时前
TMF框架与Frida脚本相关疑问及核心解析提炼
开发语言·网络·python
ArturiaZ2 小时前
【day28】
开发语言·c++·算法
我是伪码农2 小时前
Vue 2.11
前端·javascript·vue.js