目录

一、前言

在上一篇笔记实现 STM32H5 中控直接访问多 F030 传感器的基础上,本次完成功能升级:通过 PC 上位机统一访问并管控所有传感器。PC 上位机通过 USB 与 H5 中控通信,H5 作为 Modbus 中转节点,将上位机的指令映射到对应传感器的寄存器,同时把各传感器的采集数据汇总后回传给上位机,最终实现 PC 对开关量、环境监测、温湿度三类传感器的集中管控与数据查看。

二、核心设计:寄存器映射规则

H5 中控作为上位机与传感器的中间层,需为所有传感器的寄存器分配统一的地址空间,建立 “上位机寄存器地址 - 传感器寄存器地址” 的映射关系,实现指令 / 数据的中转:

  • DI 寄存器:共 3 个(地址 0~2),对应开关量传感器的 3 个按键状态;

  • DO 寄存器:共 16 个(地址 0~15),分配规则如下:

    • 地址 0:H5 自身 LED;
    • 地址 1~4:开关量传感器的 DO 寄存器;
    • 地址 6~9:环境监测传感器的 DO 寄存器;
    • 地址 11~14:温湿度传感器的 DO 寄存器;
  • AI 寄存器:共 4 个(地址 0~3),分配规则如下:

    • 地址 0~1:环境监测传感器的两路 ADC 值(光强 / 可调电阻);
    • 地址 2~3:温湿度传感器的温度、湿度值。

映射核心:H5 中控将自身统一的寄存器地址与各传感器的私有寄存器地址一一对应,上位机只需操作 H5 的寄存器,由 H5 完成指令 / 数据的转发与汇总。

三、任务规划与系统资源配置

1. 任务规划

为实现寄存器映射与数据中转,H5 中控创建 4 个核心任务:

  1. 任务 1(Modbus 服务器任务):负责与上位机通信,接收上位机指令并更新本地寄存器,同时将传感器数据回传给上位机;
  2. 任务 2(CH1 开关量传感器客户端):读取开关量传感器按键值更新 DI 寄存器,将 DO 寄存器 1~4 的值下发给开关量传感器;
  3. 任务 3(CH1 环境监测传感器客户端):将 DO 寄存器 6~9 的值下发给环境监测传感器,读取其 ADC 值更新 AI 寄存器 0~1;
  4. 任务 4(CH2 温湿度传感器客户端):将 DO 寄存器 11~14 的值下发给温湿度传感器,读取其温湿度值更新 AI 寄存器 2~3。

关键注意:CH1 通道同时访问开关量、环境监测两台传感器,需添加互斥保护,避免通信冲突。

2. 系统资源调整

由于创建多个任务且分配的栈空间较多,需增大 FreeRTOS 堆大小防止程序崩溃:在 CubeMX 中找到「中间件→X-CUBE-FREERTOS→Memory mangement settings→TOTAL_HEAP_SIZE」,将其修改为 81920。

四、程序实现:多任务 + 互斥保护 + Modbus 中转

1. 全局变量定义

先定义互斥量、二进制信号量及全局 Modbus 相关变量,用于任务间同步与互斥:

c

运行

// 二进制信号量:上位机更新DO时唤醒开关量传感器任务
static SemaphoreHandle_t g_xBinarySemaphoreSwitch;
// 二进制信号量:上位机更新DO时唤醒环境监测传感器任务
static SemaphoreHandle_t g_xBinarySemaphoreENV;
// 二进制信号量:上位机更新DO时唤醒温湿度传感器任务
static SemaphoreHandle_t g_xBinarySemaphoreTempHumi;
// 互斥量:保护CH1通道,避免两台传感器同时通信
static SemaphoreHandle_t g_ch1_lock;

// 全局寄存器映射结构体:存储DI/DO/AI寄存器值,供所有任务访问
modbus_mapping_t *g_mb_mapping;

// 全局CH1通道上下文:供开关量、环境监测任务复用
static modbus_t *g_ch1_ctx;

2. 任务 1:上位机通信(Modbus 服务器任务)

该任务作为 H5 中控的 Modbus 服务器,与 PC 上位机通信,处理寄存器读写请求,并在 DO 寄存器变化时唤醒对应传感器任务:

c

运行

// 任务1:Modbus服务器任务(与上位机通信)
// 功能:接收上位机指令→更新本地寄存器→DO变化时唤醒传感器任务→控制H5自身LED
static void LibmodbusServerTask( void *pvParameters )	
{
	uint8_t *query;          // Modbus请求报文缓冲区
	modbus_t *ctx;           // USB虚拟串口的Modbus RTU上下文
	int rc;                  // 函数返回值,判断操作是否成功
	modbus_mapping_t *mb_mapping; // 本地寄存器映射结构体
    uint8_t do_registers_backup[16]; // DO寄存器备份,用于检测值变化
	
	// 创建USB虚拟串口的Modbus上下文:波特率115200、无校验、8数据位、1停止位
	ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);
	modbus_set_slave(ctx, 1); // 设置H5中控的Modbus从机地址为1
	// 分配请求报文缓冲区(FreeRTOS内存接口)
	query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);

	// 初始化寄存器映射:DO(16)/DI(3)/AO(0)/AI(4)
	mb_mapping = modbus_mapping_new_start_address(0,
												  16,  /* DO: H5(1)+开关量(5)+环境监测(5)+温湿度(5) */
												  0,
												  3,   /* DI: 开关量传感器3个按键 */
												  0,
												  0,   /* AO: 未使用 */
												  0,
												  4);  /* AI: 环境监测(2)+温湿度(2) */
	g_mb_mapping = mb_mapping; // 赋值给全局变量,供其他任务访问
	// 初始化所有寄存器值为0
	memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
    memset(do_registers_backup, 0, sizeof(do_registers_backup));
	memset(mb_mapping->tab_registers, 0, mb_mapping->nb_registers*2);    

	// 建立与上位机的Modbus连接
	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);; // 连接失败则删除任务
	}

	// 无限循环处理上位机请求
	for (;;) {
		do {
			// 接收上位机请求报文,过滤无效请求(返回0则继续等待)
			rc = modbus_receive(ctx, query);
			/* Filtered queries return 0 */
		} while (rc == 0);
 
		/* 非CRC错误不关闭连接,仅跳过本次处理 */
		if (rc == -1 && errno != EMBBADCRC) {
			/* Quit */
			continue;
		}

		// 解析请求并回复上位机,返回当前寄存器数据
		rc = modbus_reply(ctx, query, rc, mb_mapping);
		if (rc == -1) {
			//break;
		}

        /* 检测DO寄存器变化,唤醒对应传感器任务下发指令 */
        // 开关量传感器DO(1~5)变化:唤醒任务2
        if (memcmp(&do_registers_backup[1], &mb_mapping->tab_bits[1], 5) != 0)
        {
            xSemaphoreGive(g_xBinarySemaphoreSwitch);
        }
        // 环境监测传感器DO(6~9)变化:唤醒任务3
        if (memcmp(&do_registers_backup[6], &mb_mapping->tab_bits[6], 5) != 0)
        {
            xSemaphoreGive(g_xBinarySemaphoreENV);
        }
        // 温湿度传感器DO(11~14)变化:唤醒任务4
        if (memcmp(&do_registers_backup[11], &mb_mapping->tab_bits[11], 5) != 0)
        {
            xSemaphoreGive(g_xBinarySemaphoreTempHumi);
        }

        // 备份当前DO值,用于下次对比
        memcpy(do_registers_backup, mb_mapping->tab_bits, 16);

		// 控制H5自身LED(DO寄存器0):1-亮(低电平)、0-灭(高电平)
		if (mb_mapping->tab_bits[0])
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
		else
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);
	}

	// 释放资源(实际循环不会执行到此处)
	modbus_mapping_free(mb_mapping);
	vPortFree(query);
	modbus_close(ctx);
	modbus_free(ctx);
	vTaskDelete(NULL);
}

3. 任务 2:CH1 开关量传感器客户端

该任务读取开关量传感器按键值更新 DI 寄存器,并在 DO 寄存器变化时下发指令:

c

运行

// 任务2:CH1开关量传感器客户端任务
// 功能:读取按键状态更新DI寄存器→接收DO更新指令→下发给开关量传感器
static void LibmodbusCH1SwitchClientTask( void *pvParameters )	
{
	modbus_t *ctx;           // 复用CH1全局上下文
	int rc;                  // 函数返回值
	uint16_t vals[10];       // 备用:存储寄存器值
	int nb = 1;              // 备用:寄存器数量
    uint8_t bits[10];        // 存储按键/DO状态
    uint8_t buf[100];        // LCD显示缓冲区
    int led_status = 1;      // 备用:LED状态

    ctx = g_ch1_ctx; // 复用CH1全局上下文

    // 初始化:同步开关量传感器当前DO值到本地寄存器
    xSemaphoreTake(g_ch1_lock, portMAX_DELAY); // 获取CH1互斥量,保护通信
    modbus_set_slave(ctx, 1); // 设置从机地址为1(开关量传感器)
    rc = modbus_read_bits(ctx, 0, 5, bits); // 读取5个DO寄存器
    if (rc == 5)
    {
        memcpy(&g_mb_mapping->tab_bits[1], bits, 5); // 同步到本地DO 1~5
    }
    xSemaphoreGive(g_ch1_lock); // 释放互斥量

	// 无限循环采集数据+处理指令
	for (;;) {
        /* 读取开关量传感器按键状态,更新DI寄存器并显示 */
        xSemaphoreTake(g_ch1_lock, portMAX_DELAY); // 保护CH1通道
        modbus_set_slave(ctx, 1);
        // 读取3个按键状态(DI 0~2)
        rc = modbus_read_input_bits(ctx, 0, 3, bits);
        if (rc == 3)
        {
            // LCD显示按键状态
            sprintf(buf, "SWITCH keys: %d %d %d", bits[0], bits[1], bits[2]);
            Draw_String(0, 0, buf, 0xff0000, 0);
            // 更新本地DI寄存器(0~2)
            memcpy(g_mb_mapping->tab_input_bits, bits, 3);
        }
        xSemaphoreGive(g_ch1_lock); // 释放互斥量
        
        /* 等待上位机更新DO,收到信号则下发指令 */
        if (xSemaphoreTake(g_xBinarySemaphoreSwitch, 500) == pdTRUE)
        {
            xSemaphoreTake(g_ch1_lock, portMAX_DELAY);
            modbus_set_slave(ctx, 1);
            // 将本地DO 1~5写入开关量传感器
            rc = modbus_write_bits(ctx, 0, 5, &g_mb_mapping->tab_bits[1]);            
            xSemaphoreGive(g_ch1_lock);
        }
	}

	// 释放资源(实际不会执行)
	modbus_close(ctx);
	modbus_free(ctx);
	vTaskDelete(NULL);
}

4. 任务 3:CH1 环境监测传感器客户端

该任务读取环境监测传感器 ADC 值更新 AI 寄存器,并在 DO 寄存器变化时下发指令:

c

运行

// 任务3:CH1环境监测传感器客户端任务
// 功能:读取ADC值更新AI寄存器→接收DO更新指令→下发给环境监测传感器
static void LibmodbusCH1ENVClientTask( void *pvParameters )	
{
	modbus_t *ctx;           // 复用CH1全局上下文
	int rc;                  // 函数返回值
	uint16_t vals[10];       // 存储ADC值(16位)
	int nb = 1;              // 备用:寄存器数量
    uint8_t bits[10];        // 存储DO状态
    uint8_t buf[100];        // LCD显示缓冲区
    int led_status = 1;      // 备用:LED状态

    ctx = g_ch1_ctx; // 复用CH1全局上下文

    // 初始化:同步环境监测传感器当前DO值到本地寄存器
    xSemaphoreTake(g_ch1_lock, portMAX_DELAY);
    modbus_set_slave(ctx, 2); // 从机地址2(环境监测传感器)
    rc = modbus_read_bits(ctx, 0, 5, bits); // 读取5个DO寄存器
    if (rc == 5)
    {
        memcpy(&g_mb_mapping->tab_bits[6], bits, 5); // 同步到本地DO 6~9
    }
    xSemaphoreGive(g_ch1_lock);

	// 无限循环采集数据+处理指令
	for (;;) {
        /* 读取环境监测传感器ADC值,更新AI寄存器并显示 */
        xSemaphoreTake(g_ch1_lock, portMAX_DELAY); // 保护CH1通道
        modbus_set_slave(ctx, 2);
        // 读取2个ADC值(AI 0~1)
		rc = modbus_read_input_registers(ctx, 0, 2, vals);
		if (rc == 2)
		{
            // LCD显示ADC值
            sprintf(buf, "ENV Sensor : opti 0x%x, res 0x%x          ", vals[0], vals[1]);
            Draw_String(0, 16, buf, 0xff0000, 0);
            // 更新本地AI寄存器(0~1)
            memcpy(g_mb_mapping->tab_input_registers, vals, 4);
		}
        xSemaphoreGive(g_ch1_lock);

        /* 等待上位机更新DO,收到信号则下发指令 */
        if (xSemaphoreTake(g_xBinarySemaphoreENV, 500) == pdTRUE)
        {
            xSemaphoreTake(g_ch1_lock, portMAX_DELAY);
            modbus_set_slave(ctx, 2);
            // 将本地DO 6~9写入环境监测传感器
            rc = modbus_write_bits(ctx, 0, 5, &g_mb_mapping->tab_bits[6]);            
            xSemaphoreGive(g_ch1_lock);
        }
	}

	// 释放资源(实际不会执行)
	modbus_close(ctx);
	modbus_free(ctx);
	vTaskDelete(NULL);
}

5. 任务 4:CH2 温湿度传感器客户端

该任务读取温湿度传感器数据更新 AI 寄存器,并在 DO 寄存器变化时下发指令:

c

运行

// 任务4:CH2温湿度传感器客户端任务
// 功能:读取温湿度值更新AI寄存器→接收DO更新指令→下发给温湿度传感器
static void LibmodbusCH2TempHumiClientTask( void *pvParameters )	
{
	modbus_t *ctx;           // CH2通道Modbus上下文
	int rc;                  // 函数返回值
	uint16_t vals[10];       // 存储温湿度值(16位)
	int nb = 1;              // 备用:寄存器数量
    uint8_t bits[10];        // 存储DO状态
    uint8_t buf[100];        // LCD显示缓冲区
    int led_status = 1;      // 备用:LED状态
	
	// 创建CH2通道上下文(串口4)
	ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);
	modbus_set_slave(ctx, 3); // 从机地址3(温湿度传感器)
	
	// 建立Modbus连接
	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);;
	}

    // 初始化:同步温湿度传感器当前DO值到本地寄存器
    rc = modbus_read_bits(ctx, 0, 5, bits);
    if (rc == 5)
    {
        memcpy(&g_mb_mapping->tab_bits[11], bits, 5); // 同步到本地DO 11~14
    }

	// 无限循环采集数据+处理指令
	for (;;) {
        /* 读取温湿度值,更新AI寄存器并显示 */
		rc = modbus_read_input_registers(ctx, 0, 2, vals); // 读取温度、湿度
		if (rc == 2)
		{
            // LCD显示温湿度(单位:0.1℃、0.1%RH)
            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);
            // 更新本地AI寄存器(2~3)
            memcpy(&g_mb_mapping->tab_input_registers[2], vals, 4);
		}

        /* 等待上位机更新DO,收到信号则下发指令 */
        if (xSemaphoreTake(g_xBinarySemaphoreTempHumi, 500) == pdTRUE)
        {
            // 将本地DO 11~14写入温湿度传感器
            rc = modbus_write_bits(ctx, 0, 5, &g_mb_mapping->tab_bits[11]);            
        }
	}

	// 释放资源(实际不会执行)
	modbus_close(ctx);
	modbus_free(ctx);
	vTaskDelete(NULL);
}

6. FreeRTOS 任务初始化

在 FreeRTOS 初始化函数中创建信号量、互斥量及所有任务:

c

运行

// FreeRTOS初始化函数:创建信号量、互斥量及所有任务
void MX_FREERTOS_Init(void) {
  // 创建二进制信号量(DO变化通知)
  g_xBinarySemaphoreSwitch = xSemaphoreCreateBinary( );
  g_xBinarySemaphoreENV    = xSemaphoreCreateBinary( );
  g_xBinarySemaphoreTempHumi = xSemaphoreCreateBinary( );
  // 创建CH1通道互斥量
  g_ch1_lock = xSemaphoreCreateMutex();

  /* 任务1:Modbus服务器(与上位机通信) */
  xTaskCreate(
      LibmodbusServerTask, 
      "LibmodbusServerTask", 
      300,  // 任务栈大小
      NULL, 
      osPriorityNormal,  // 任务优先级
      NULL); 

  // 初始化CH1全局上下文(供任务2、3复用)
  g_ch1_ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);
  modbus_set_slave(g_ch1_ctx, 1);
  // 建立CH1连接
  if (modbus_connect(g_ch1_ctx) == -1) {
      modbus_free(g_ch1_ctx);
      return;
  }

  /* 任务2:CH1开关量传感器客户端 */
  xTaskCreate(
      LibmodbusCH1SwitchClientTask,
      "LibmodbusCH1SwitchClientTask",
      300,
      NULL,
      osPriorityNormal,
      NULL);

  /* 任务3:CH1环境监测传感器客户端 */
  xTaskCreate(
      LibmodbusCH1ENVClientTask,
      "LibmodbusCH1ENVClientTask",
      300,
      NULL,
      osPriorityNormal,
      NULL);

  /* 任务4:CH2温湿度传感器客户端 */
  xTaskCreate(
      LibmodbusCH2TempHumiClientTask,
      "LibmodbusCH2TempHumiClientTask",
      300,
      NULL,
      osPriorityNormal,
      NULL);
}

五、功能验证:上位机管控多传感器

完成程序编译烧录后,使用 Modbus Poll 作为 PC 上位机工具,通过 USB 连接 H5 中控,可实现:

  • 读取 DI 寄存器:查看开关量传感器的 3 个按键状态;
  • 读取 AI 寄存器:查看环境监测传感器的 ADC 值、温湿度传感器的温湿度值;
  • 改写 DO 寄存器:远程控制开关量、环境监测、温湿度传感器的蜂鸣器 / LED,以及 H5 自身 LED。

实际测试效果如下:

请添加图片描述

六、总结

  1. 基于寄存器映射实现上位机对多传感器的统一管控,H5 中控作为中转简化上位机操作逻辑;
  2. 采用 FreeRTOS 多任务调度,每个传感器对应独立任务,保障数据采集 / 指令下发的并行性;
  3. 通过互斥量保护共享通信通道、二进制信号量实现任务同步,避免冲突与数据异常;
  4. 调整 FreeRTOS 堆大小,解决多任务栈分配导致的程序崩溃问题。

七、结尾

本次完成了 PC 上位机对多传感器的集中管控开发,实现了从 “本地中控访问” 到 “远程上位机管控” 的升级,这套寄存器映射 + 多任务互斥的设计思路,可直接应用于工业级多传感器远程监测与控制系统。掌握 Modbus 中转、任务同步与资源保护的核心逻辑,是构建复杂嵌入式管控系统的关键。感谢各位的阅读,持续关注本系列笔记,一起探索更多嵌入式与工业通信的实用开发技巧!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐