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 总结与注意事项
-
地址映射是关键:必须精心设计H5的寄存器地址与各个传感器寄存器的对应关系,避免冲突。
-
多任务同步:全局映射被多个任务读写,需要确保数据一致性。本例中,读和写是分离的,且任务间没有同时写同一位置(不同任务写不同区域),因此暂无需互斥。但如果存在多个任务写同一寄存器,则需加锁。
-
LCD互斥:任何使用LCD的任务必须获取互斥信号量,防止显示混乱。
-
从站地址管理:H5自身地址为1,三个传感器地址分别为1、2、3,注意传感器地址1与H5自身地址相同,但不在同一总线上(传感器1在CH1,H5自身在PC串口),所以不冲突。但如果同一总线上有两个相同地址的设备就会冲突,这里CH1上的两个传感器地址不同(1和2),没问题。
-
错误处理:代码中简化为如果连接失败则删除任务,实际应增加重连机制。
-
扩展性:可以轻松添加更多传感器,只需增加任务和映射表项。
通过本章学习,你掌握了如何设计一个Modbus网关,将多个从设备的数据汇集到一个主控,再统一对外提供服务。这是工业物联网中非常常见的应用模式。