环境监测传感器从设备程序设计(ADC采集与输出控制)

9.1 项目概述

本章设计一个环境监测传感器从设备,具有以下硬件资源:

  • 2个有源蜂鸣器(BEEP1、BEEP2):高电平发声。

  • 3个LED(LED1~LED3):低电平点亮。

  • 1路光敏电阻(OPTO_ADC):通过ADC采集电压,电压值与光强成反比。

  • 1路可调电阻(RES_ADC):通过ADC采集电压,电压值与电阻值成反比。

设备通过RS485接口以Modbus RTU协议与主站通信,从站地址设为02H

9.2 硬件引脚定义

根据提供的表格,硬件连接如下:

功能 引脚 电平逻辑 Modbus类型
BEEP1(蜂鸣器1) PB15 高电平发声 DO
BEEP2(蜂鸣器2) PB14 高电平发声 DO
LED1 PB11 低电平发光 DO
LED2 PB12 低电平发光 DO
LED3 PB13 低电平发光 DO
OPTO_ADC(光敏) PA1 ADC输入(电压与光强成反比) AI(输入寄存器)
RES_ADC(可调电阻) PA2 ADC输入(电压与电阻成反比) AI(输入寄存器)

电平逻辑总结

  • 蜂鸣器:高电平驱动(1→响,0→静音)

  • LED:低电平驱动(1→亮,0→灭?注意表格中LED是"低电平发光",所以当输出低电平时LED亮。但寄存器描述中"1-亮"对应我们需要输出低电平,所以代码中需要取反)

9.3 Modbus寄存器地址分配

根据需求,设备地址为02H,寄存器分配如下:

设备地址 寄存器地址 寄存器类别 用途 描述(位值)
02H 0000H DO 控制蜂鸣器1 1-响
0001H DO 控制蜂鸣器2 1-响
0002H DO 控制LED1 1-亮
0003H DO 控制LED2 1-亮
0004H DO 控制LED3 1-亮
02H 0000H AI 读取光敏电压 12位ADC值,0~4095对应0~3.3V
0001H AI 读取可调电阻电压 12位ADC值,0~4095对应0~3.3V

注意

  • 线圈(DO)使用地址0x0000~0x0004,共5个。

  • 输入寄存器(AI)使用地址0x0000~0x0001,共2个。输入寄存器是16位的,存放ADC转换结果(12位有效,高4位补0)。

9.4 代码整体结构分析

代码基于STM32 HAL库和FreeRTOS,与上一章类似,但使用了ADC模块。

9.4.1 头文件与宏定义
cs 复制代码
#ifdef USE_ENV_MONITOR_SENSOR
extern ADC_HandleTypeDef hadc;  // 引用HAL库的ADC句柄

#define SLAVE_ADDR 2
#define NB_BITS             5   // 线圈数量(蜂鸣器2 + LED3 = 5)
#define NB_INPUT_BITS       0   // 离散输入数量(无)
#define NB_REGISTERS        0   // 保持寄存器数量(无)
#define NB_INPUT_REGISTERS  2   // 输入寄存器数量(光敏+可调电阻)
#endif

解释

  • NB_BITS = 5:对应地址0x0000~0x0004,5个线圈输出。

  • NB_INPUT_REGISTERS = 2:对应地址0x0000~0x0001,2个输入寄存器。

  • extern ADC_HandleTypeDef hadc:ADC句柄,需要在其他地方定义并初始化(通常在HAL生成的代码中)。

9.4.2 任务函数中的ADC读取部分

在收到请求后,更新输入寄存器(读取ADC)的代码:

cs 复制代码
#ifdef USE_ENV_MONITOR_SENSOR
    /* READ ADC to update tab_input_registers */
    for (int i = 0; i < 2; i++)
    {
        HAL_ADC_Start(&hadc);                     // 启动ADC转换
        if (HAL_OK == HAL_ADC_PollForConversion(&hadc, 100))  // 等待转换完成,超时100ms
        {
            mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc); // 读取转换结果
        }
    }
#endif

详细解释

  1. HAL_ADC_Start(&hadc):启动ADC转换(单次模式)。

  2. HAL_ADC_PollForConversion(&hadc, 100):轮询等待转换完成,超时100ms。如果返回HAL_OK,表示转换成功。

  3. HAL_ADC_GetValue(&hadc):获取转换结果,通常是12位值(0~4095)。

  4. 将结果存入mb_mapping->tab_input_registers[i],i=0对应光敏,i=1对应可调电阻。

注意:这里每次收到请求后都会重新启动ADC转换,这意味着主站每次读取输入寄存器都会触发一次新的ADC采样。如果ADC采样需要稳定时间,可能需要在循环外定期更新,但这里简单处理为每次请求时更新。

9.4.3 输出控制部分(根据线圈状态更新硬件)

在回复请求后,根据tab_bits的值控制蜂鸣器和LED:

cs 复制代码
#ifdef USE_ENV_MONITOR_SENSOR
    /* beep1 */
    if (mb_mapping->tab_bits[0])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); // beep1 高电平响
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);

    /* beep2 */
    if (mb_mapping->tab_bits[1])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);

    /* LED1 */
    if (mb_mapping->tab_bits[2])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); // LED1 低电平亮
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);

    /* LED2 */
    if (mb_mapping->tab_bits[3])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);

    /* LED3 */
    if (mb_mapping->tab_bits[4])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
#endif

电平转换说明

  • 蜂鸣器:tab_bits为1时,输出高电平(GPIO_PIN_SET);为0时输出低电平(GPIO_PIN_RESET)。符合"1-响"的定义。

  • LED:tab_bits为1时,输出低电平(GPIO_PIN_RESET);为0时输出高电平(GPIO_PIN_SET)。这是因为硬件是低电平点亮,所以逻辑1对应亮,硬件上需要输出低电平。

9.4.4 完整的任务循环结构

任务函数整体框架与第八章类似,但这里使用了输入寄存器,且无离散输入。关键部分如下:

cs 复制代码
void StartDefaultTask(void *argument)
{
    // 初始化...
    ctx = modbus_new_rtu("uart1", 115200, 'N', 8, 1);
    modbus_set_slave(ctx, SLAVE_ADDR);
    query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);

    // 创建映射:线圈起始0数量5,离散输入0数量0,输入寄存器0数量2,保持寄存器0数量0
    mb_mapping = modbus_mapping_new_start_address(0, NB_BITS,
                                                  0, NB_INPUT_BITS,
                                                  0, NB_INPUT_REGISTERS,
                                                  0, NB_REGISTERS);

    rc = modbus_connect(ctx);
    if (rc == -1) {
        // 错误处理
    }

    for (;;) {
        do {
            rc = modbus_receive(ctx, query);
        } while (rc == 0);

        if (rc == -1 && errno != EMBBADCRC) {
            continue;
        }

        // 更新输入寄存器(ADC采样)
        for (int i = 0; i < NB_INPUT_REGISTERS; i++) {
            HAL_ADC_Start(&hadc);
            if (HAL_ADC_PollForConversion(&hadc, 100) == HAL_OK) {
                mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc);
            }
        }

        // 回复请求
        rc = modbus_reply(ctx, query, rc, mb_mapping);
        if (rc == -1) {
            // 错误处理
        }

        // 根据线圈状态更新硬件输出
        // ...(上面已列出)
    }

    // 清理资源
    modbus_mapping_free(mb_mapping);
    vPortFree(query);
    modbus_close(ctx);
    modbus_free(ctx);
    vTaskDelete(NULL);
}

9.5 关键细节深入解析

9.5.1 ADC读取的注意事项
  1. ADC初始化 :需要确保ADC已经在MX_ADC_Init()中正确配置(时钟、通道、分辨率、对齐方式等)。代码中hadc是HAL库生成的句柄,需包含对应头文件。

  2. 转换模式:使用单次转换模式,每次启动后轮询等待完成。如果ADC采样时间较长,轮询超时时间要足够(这里100ms可能过大,但安全)。

  3. 通道切换 :这里只有一个ADC句柄,假设已经配置为扫描模式或通过软件依次选择通道?代码中for循环两次,每次都启动同一个hadc,但并没有切换通道。这通常是不正确的,因为hadc可能只配置了一个通道。正确的做法是需要配置ADC为多通道规则组,或者使用两个不同的ADC句柄,或者在启动前设置通道。

    假设实际硬件有两个独立的ADC通道(如PA1和PA2),它们可能属于同一个ADC模块的不同通道。正确代码应该:

    • 方法1:使用ADC的扫描模式,一次启动连续转换两个通道,然后依次读取结果。

    • 方法2:在每次启动前设置转换通道(通过HAL_ADC_ConfigChannel)。

    但当前代码简单地在循环中启动同一个ADC,可能导致两次读取的都是同一个通道(最后一次配置的通道)。这需要根据实际硬件配置来判断。如果两个通道是独立的ADC实例(例如ADC1和ADC2),则应该有两个句柄。但从代码看只有一个hadc,可能实际应用中已经将两个通道配置为扫描模式,每次启动后读取两个结果,但这里却分别启动两次,会覆盖结果。所以这是一个潜在的bug或简化。

    在讲解时,可以指出这个问题,并说明正确做法。

  4. 结果存储 :ADC值是12位,存入16位的tab_input_registers是合适的。主站读取时获得原始ADC值,需要自己转换成物理量(如电压)。

9.5.2 输出控制的电平极性
  • 蜂鸣器:高电平有效,所以tab_bits直接对应引脚电平。

  • LED:低电平有效,所以tab_bits为1时引脚应置低,为0时置高。代码中正确实现了取反逻辑。

9.5.3 地址映射关系
  • 线圈地址0x0000对应tab_bits[0](蜂鸣器1)

  • 线圈地址0x0001对应tab_bits[1](蜂鸣器2)

  • 线圈地址0x0002对应tab_bits[2](LED1)

  • 线圈地址0x0003对应tab_bits[3](LED2)

  • 线圈地址0x0004对应tab_bits[4](LED3)

  • 输入寄存器地址0x0000对应tab_input_registers[0](光敏ADC)

  • 输入寄存器地址0x0001对应tab_input_registers[1](可调电阻ADC)

9.5.4 更新时机
  • 输入寄存器更新:每次收到有效请求后立即更新,保证主站读到最新采样值。

  • 输出更新:在回复后立即更新,使主站的写操作立即生效。

9.6 完整点表总结

类型 Modbus地址 PLC地址(常见) 名称 访问 说明
线圈 0x0000 00001 蜂鸣器1 读写 1=响
线圈 0x0001 00002 蜂鸣器2 读写 1=响
线圈 0x0002 00003 LED1 读写 1=亮
线圈 0x0003 00004 LED2 读写 1=亮
线圈 0x0004 00005 LED3 读写 1=亮
输入寄存器 0x0000 30001 光敏电压ADC值 只读 0~4095对应0~3.3V
输入寄存器 0x0001 30002 可调电阻ADC值 只读 0~4095对应0~3.3V

注意:PLC地址通常从1开始,且类型前缀不同(0x对应0类,3x对应3类),这里只是示意。

9.7 调试与验证

  1. 读输入寄存器(功能码04):读取地址0~1,观察ADC值是否随光照和电位器变化。

  2. 写线圈(功能码05/15):控制蜂鸣器和LED,验证动作是否正确。

  3. 注意电平极性:LED亮时测量引脚应为低电平,蜂鸣器响时引脚应为高电平。

  4. ADC精度:12位ADC,参考电压3.3V,可通过电压表验证转换关系。

9.8 可能的改进点

  • ADC多通道正确配置:如果两个通道在同一ADC,应使用扫描模式,一次启动后读取多个结果,避免重复启动。

  • 错误处理:如果ADC转换超时,应保留旧值或返回异常。

  • 输入寄存器更新频率:可以在任务空闲时定时更新,而不是每次请求都重新采样,避免频繁启动ADC影响实时性。

9.9 本章核心知识点总结

  1. 输入寄存器(AI)的使用:用于传输16位的模拟量数据,如ADC采样值。

  2. ADC读取流程:启动转换→等待完成→读取结果→存入映射。

  3. 电平极性转换:根据硬件电路正确映射逻辑值到物理电平(特别注意LED低电平点亮的情况)。

  4. 多通道ADC处理:需要注意切换通道或使用扫描模式。

  5. Modbus寄存器地址分配:线圈和输入寄存器地址空间独立,通过不同功能码访问。

  6. 从设备程序结构:初始化→创建映射→连接→循环接收请求→更新输入→回复→更新输出。

相关推荐
张槊哲2 小时前
IIC时序图详解
单片机
Hello_Embed2 小时前
Modbus 传感器开发:STM32F030 串口编程
笔记·stm32·单片机·嵌入式·freertos·modbus
cameron_tt3 小时前
stm32智能垃圾桶
stm32·单片机·嵌入式硬件
犽戾武3 小时前
在 Quest 上用 OpenXR + MediaCodec + OES 外部纹理做一个“低延迟视频面板”(48小时的编码复盘)
linux·c++·嵌入式硬件·vr
dadaobusi3 小时前
verilog,generate语句
fpga开发
myron66883 小时前
基于STM32LXXX的模数转换芯片ADC(ADS1110A0IDBVR)驱动C程序设计
c语言·stm32·嵌入式硬件
广药门徒4 小时前
PADS Layout里的条件筛选在Router里在哪找
嵌入式硬件
玩转单片机与嵌入式4 小时前
TinyML适合在哪些MCU上运行?怎么评估我们的MCU能不能运行TinyML?(赠评估手册)
单片机·嵌入式硬件
亿道电子Emdoor12 小时前
【Arm】Keil MDK 的Symbols窗口
stm32·单片机·嵌入式硬件