传感器课程学习周记:三大传感器实验深度解析

本周的传感器原理与应用技术课程,我完成了三个经典的传感器实验——光强检测、可燃气体检测和光电开关检测。从I2C数字通信到GPIO模拟输入,从半导体气敏原理到红外光电检测,每一个实验都让我对"感知层是物联网的眼睛和皮肤"这句话有了更深刻的理解。

一、光强传感器实验(BH1750)

实验目标

第一个实验是基于BH1750FVI数字光强传感器的环境光照检测。核心目标是掌握I2C总线通信原理,学会通过软件I2C(GPIO模拟)读取传感器数据,并理解照度(lux)的换算方法。

核心原理

BH1750传感器概述:这是ROHM公司生产的数字型环境光传感器IC,采用I2C总线接口,测量范围1-65535 lx。内部集成了光电二极管、积分运放、16位ADC和I2C接口,光谱响应曲线接近人眼,峰值波长560nm。

照度计算公式:

Illuminance(lx) = raw_data / 1.2

高分辨率模式下,16位原始ADC值除以1.2即可得到勒克斯值。这个1.2的系数来自数据手册的Measurement Accuracy典型值。

软件I2C通信:实验没有使用STM32的硬件I2C外设,而是用GPIO模拟I2C时序——SCL接PB13,SDA接PB14。SDA引脚通过宏动态切换输入/输出模式,这是半双工通信的关键技巧。通信时序包括:

  • Start信号:SCL高电平时SDA下降沿
  • Stop信号:SCL高电平时SDA上升沿
  • 数据传输:SCL高电平期间采样SDA,MSB先发
  • ACK应答:每8位数据后等待从机拉低SDA

I2C从机地址:BH1750支持两种地址,由ADDR引脚电平决定。本实验使用ADDR=LOW对应的0x23(7位地址),写地址为0x46(0x23<<1 | 0),读地址为0x47(0x23<<1 | 1)。

关键代码实现

指令集与地址定义:

// bh1750.c 第38-45行
#define BH1750_ADD_WRITE  0x46   // 写数据(地址0x23<<1 | 0)
#define BH1750_ADD_READ   0x47   // 读数据(地址0x23<<1 | 1)
#define BH1750_POWER_ON   0x01   // 打开模块
#define BH1750_POWER_DOWN 0x00   // 关闭模块
#define BH1750_MODE_H1    0x10   // 高分辨率1,单位1lx
#define BH1750_MODE_H2    0x11   // 高分辨率2,单位0.5lx
#define BH1750_MODE_L     0x13   // 低分辨率,单位4lx

GPIO初始化:

// bh1750.c 第105-126行
static void BH1750_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    // PB13(SCL) + PB14(SDA) 配置为推挽输出+上拉
    GPIO_InitStruct.Pin = BH1750_SCL_Pin | BH1750_SDA_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(BH1750_SCL_GPIO_Port, &GPIO_InitStruct);
}

SDA方向动态切换宏:

#define BH1750_SDA_IN()  {GPIOB->MODER&=~(3<<(14*2)); GPIOB->MODER|=0<<14*2;}
#define BH1750_SDA_OUT() {GPIOB->MODER&=~(3<<(14*2)); GPIOB->MODER|=1<<14*2;}

发送指令(写报文):

// bh1750.c 第310-318行
static void BH1750_Write_Command(uint8_t cmd) {
    BH1750_IIC_Start();                    // ST: 起始信号
    BH1750_Write_Byte(BH1750_ADD_WRITE);   // 0100011 + 0 = 0x46(写)
    BH1750_IIC_Wait_Ack();                 // Ack: 等待应答
    BH1750_Write_Byte(cmd);                // 指令码
    BH1750_IIC_Wait_Ack();                 // Ack: 等待应答
    BH1750_IIC_Stop();                     // SP: 停止信号
}

对应数据手册报文格式:ST | 0100011 | 0(W) | Ack | 00010000 | Ack | SP

读取数据(读报文):

// bh1750.c 第355-369行
static uint16_t Read_BH1750(void) {
    uint16_t light_buf = 0;
    BH1750_IIC_Start();                    // ST: 起始信号
    BH1750_Write_Byte(BH1750_ADD_READ);    // 0100011 + 1 = 0x47(读)
    BH1750_IIC_Wait_Ack();                 // Ack: 等待应答
    light_buf = BH1750_Read_Byte(ACK);     // High Byte + 发送ACK
    light_buf <<= 8;                       // 左移8位
    light_buf |= BH1750_Read_Byte(NOACK);  // Low Byte + 发送NACK
    BH1750_IIC_Stop();                     // SP: 停止信号
    return light_buf;                      // 返回16位原始数据
}

对应数据手册报文格式:ST | 0100011 | 1(R) | Ack | High Byte | Ack | Low Byte | NACK | SP

完整测量流程:

uint16_t BH1750_Handle(void) {
    uint16_t result_lx = 0;
    Start_BH1750();          // 发送上电(0x01) + 高分辨率模式(0x10)
    delay_ms(180);           // 等待最大180ms确保测量完成
    result_lx = Read_BH1750(); // 读取16位原始数据
    result_lx = (float)(result_lx / 1.2); // 照度换算
    return result_lx;
}

180ms取自数据手册H-Resolution Mode测量时间的最大值(典型120ms,最大180ms),用最大值作为等待时间确保最坏情况下测量也能完成。

三种场景实测结果

场景光强范围(lx)稳定值(lx)波动范围
正常环境光469 - 470470±1 lx
模拟强光2570 - 68686864±8 lx
模拟暗光7 - 47725±0 lx

正常室内日光灯环境下读数约470 lx,符合300-500 lx的室内照明标准;手机闪光灯照射下达到6864 lx,远未达到65535 lx的量程上限;用手遮挡时最低可达7 lx,验证了高分辨率模式的低光灵敏度。三种场景下读数波动均小于1%,稳定性优于数据手册标称的±20%。

二、气体传感器实验(MQ-5)

实验目标

第二个实验是基于MQ-5的可燃气体检测。目标是理解半导体气敏元件的工作机制,掌握LM393比较器在信号调理中的作用,以及STM32 GPIO输入检测的基本操作。

核心原理

MQ-5工作原理:传感器采用SnO2(二氧化锡)半导体气敏材料。内部加热丝将材料加热至约300°C工作温度。清洁空气中SnO2阻值较高;当环境中存在可燃气体(LPG、甲烷、一氧化碳等)时,气体分子在材料表面发生化学吸附,电子浓度增加,阻值下降。气体浓度越高,阻值下降越明显。

五级信号处理链路:

  1. 气体感知:SnO2气敏电阻感知可燃气体浓度,阻值变化
  2. 信号转换:气敏电阻+固定电阻组成分压电路,阻值→电压(AO模拟量)
  3. 阈值比较:LM393APSR比较器将AO与电位器参考电压比较
  4. MCU读取:STM32通过PD15引脚读取数字输出(DO)
  5. 信息输出:LCD显示 + 串口输出

LM393比较器逻辑:

  • INA+(正相输入,第3脚)接MQ-5的AO模拟电压
  • INA-(反相输入,第2脚)接电位器设定的阈值参考电压
  • OUTA(第1脚)输出至STM32的PD15引脚

比较器工作逻辑:

  • 当INA+ > INA-(无气体/浓度低)→ 输出高电平(1)→ 安全
  • 当INA+ < INA-(有气体/浓度高)→ 输出低电平(0)→ 报警

电位器旋钮可以调节阈值大小,从而调整传感器灵敏度。

关键代码实现

引脚定义与初始化:

// mq_5.c
#define MQ5_Pin        GPIO_PIN_15   // PD15引脚
#define MQ5_GPIO_Port  GPIOD         // GPIOD端口
#define MQ5_STATE()    HAL_GPIO_ReadPin(MQ5_GPIO_Port, MQ5_Pin)

void MQ5_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    __HAL_RCC_GPIOD_CLK_ENABLE();   // 使能GPIOD时钟
    GPIO_InitStruct.Pin = MQ5_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;  // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;      // 上拉电阻
    HAL_GPIO_Init(MQ5_GPIO_Port, &GPIO_InitStruct);
}

状态读取函数:

uint8_t MQ5_Read_State(void) {
    return MQ5_STATE();  // 返回: 1=无可燃气体, 0=有可燃气体
}

业务处理函数:

// handle.c
void MQ5_Handle(void) {
    uint8_t mq5_detect = 0;
    static uint8_t mq2_detect_rev = 3;  // 状态缓存,初始值3(非0非1)
    static uint32_t tickstart;
    
    if((HAL_GetTick() - tickstart) >= 1000) {  // 每1秒检测一次
        tickstart = HAL_GetTick();
        
        if(MQ5_Read_State() == 0) {  // PD15低电平 → 有可燃气体
            LCD_draw_bmp(96, 120, 48, gImage_yanwu);  // 显示有气体图标
            Show_Str(85, 178, BLUE, WHITE, "有可燃气体", 16, 1);
        } else {  // PD15高电平 → 无可燃气体
            LCD_draw_bmp(96, 120, 48, gImage_yanwu_hui);  // 灰色图标
            Show_Str(85, 178, BLUE, WHITE, "无可燃气体", 16, 1);
        }
        
        mq5_detect = MQ5_Read_State();
        // 仅在状态变化时串口输出,避免刷屏
        if((!mq5_detect) != mq2_detect_rev) {
            printf("** 杨斌 combustible gas detect:%d\r\n", !mq5_detect);
            mq2_detect_rev = !mq5_detect;
        }
        
        led_toggle(LED1_PORT);  // LED1翻转指示运行
    }
}

printf串口重定向:

// usart.c
int fputc(int ch, FILE *f) {
    HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

重定向fputc后,printf输出的每个字符都会通过USART2(PA2/PA3,波特率115200)发送出去。

主函数流程:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    LCD_Display_Init();          // 屏幕初始化
    gpio_module_open(LEDS);      // LED引脚初始化
    uart_open(uart2, 115200);    // 串口初始化
    printf("\r\n 可燃气体检测实验 \r\n");
    printf("杨斌 开始本实验:\r\n\r\n");
    MQ5_Init();                  // MQ-5传感器初始化(PD15)
    while(1) {
        MQ5_Handle();            // 主循环检测
    }
}

三、光电开关传感器实验(E18-D80NK)

实验目标

第三个实验是基于E18-D80NK的红外光电开关检测。目标是掌握光电开关模块的电路原理,理解开漏输出与上拉电阻的配合关系,以及STM32 GPIO输入检测的编程方法。

核心原理

三线制接线:

JP7引脚连接到线色功能
Pin 1+5V棕色电源正极
Pin 2PB0黑色信号输出
Pin 3GND蓝色地

Pin2到+5V之间接有10K上拉电阻。

开漏输出与上拉电阻:这是本实验最重要的知识点。E18-D80NK的输出为开漏(Open Collector)结构,自身无法输出高电平,必须外接10K上拉电阻将信号线拉高到5V。MCU内部也配置了GPIO_PULLUP上拉模式,双重保障信号稳定性。

数据手册原文:“OUTPUT is Open Collector; it has to connect R 10K Pull Up at Output”

距离-输出逻辑:

  • 距离 ≤ 检测距离(有遮挡)→ LED ON → OUTPUT = 0(低电平)
  • 距离 ≥ 检测距离(无遮挡)→ LED OFF → OUTPUT = 1(高电平)

传感器利用红外反射原理检测物体,检测距离可通过尾部电位器调节(3-80cm范围)。

关键代码实现

驱动层完整代码:

// e18_d80nk.c
/* IO引脚宏定义 */
#define E18_D80NK_Pin        GPIO_PIN_0
#define E18_D80NK_GPIO_Port  GPIOB
#define E18_D80NK_STATE()    HAL_GPIO_ReadPin(E18_D80NK_GPIO_Port, E18_D80NK_Pin)

/* 初始化函数 - 配置PB0为上拉输入模式 */
void E18_D80NK_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    E18_D80NK_Port_Clk_Enable;       // 使能GPIOB时钟
    GPIO_InitStruct.Pin = E18_D80NK_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;  // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;      // 上拉(匹配开漏输出)
    HAL_GPIO_Init(E18_D80NK_GPIO_Port, &GPIO_InitStruct);
}

/* 读取传感器状态 - 返回0(有遮挡)或1(无遮挡) */
uint8_t E18_D80NK_Read_State(void) {
    return E18_D80NK_STATE();
}

业务处理函数:

// handle.c
void Infrared_Switch_Handle(void) {
    uint8_t e18_d80nk_state = 0;
    static uint8_t e18_d80nk_state_rev = 2;  // 状态缓存,初始值2(非0非1)
    static uint32_t tickstart;
    
    if((HAL_GetTick() - tickstart) >= 1000) {  // 1000ms检测一次
        tickstart = HAL_GetTick();
        e18_d80nk_state = E18_D80NK_Read_State();  // 读取传感器状态
        
        if(e18_d80nk_state == 0) {  // 有遮挡
            LCD_draw_bmp(96, 120, 48, gImage_guangdian);  // 遮挡图标
            Show_Str(96, 178, BLUE, WHITE, "有遮挡", 16, 1);
            beep_open();  // 蜂鸣器报警
        } else {  // 无遮挡
            LCD_draw_bmp(96, 120, 48, gImage_guangdianhui);  // 无遮挡图标
            Show_Str(96, 178, BLUE, WHITE, "无遮挡", 16, 1);
            beep_close();  // 关闭蜂鸣器
        }
        
        /* 状态变化时串口打印,避免刷屏 */
        if(e18_d80nk_state != e18_d80nk_state_rev) {
            printf("** 读取光电开关传感器数据\r\n");
            if(e18_d80nk_state == 0)
                printf("** 状态:有遮挡\r\n\r\n");
            else
                printf("** 状态:无遮挡\r\n\r\n");
            e18_d80nk_state_rev = e18_d80nk_state;  // 更新缓存
        }
        
        led_toggle(LED1_PORT);  // LED1闪烁指示运行
    }
}

状态变化检测机制是这段代码的亮点——用静态变量缓存上次状态,初始值设为2(既不是0也不是1),确保第一次检测时必然触发状态变化输出。后续只有状态真正改变时才打印,完美解决了刷屏问题。

四、学习收获与总结

通信方式对比

三个实验分别使用了不同的通信/读取方式,正好可以对比学习:

传感器通信方式数据类型引脚特点
BH1750软件I2C数字量(16位)PB13/PB14协议复杂,数据精度高
MQ-5GPIO输入数字量(1位)PD15简单直接,仅能判断阈值
E18-D80NKGPIO输入数字量(1位)PB0开漏输出,需上拉电阻

技术收获

  1. I2C协议深度理解:通过软件I2C的实现,真正搞懂了Start/Stop时序、ACK/NACK应答、SDA方向切换等底层细节,比直接用硬件I2C库函数收获大得多。

  2. 数据手册阅读能力:BH1750实验中对照数据手册逐一验证了指令集、I2C地址、报文格式、照度公式、测量时间等,学会了"从数据手册找依据"的工程方法。

  3. 传感器信号调理链路:MQ-5实验展示了完整的信号链——从物理量(气体浓度)→ 化学效应(阻值变化)→ 电路转换(分压电压)→ 比较器(数字阈值)→ MCU读取。这是模拟传感器数字化的典型路径。

  4. 开漏输出的工程意义:E18-D80NK的开漏输出设计让我理解了为什么工业传感器常用这种接口——可以实现电平转换(5V传感器→3.3V MCU)、线与逻辑、以及更高的抗干扰能力。

  5. 分层架构与代码复用:三个实验都采用了驱动层+业务层的分层设计,结构清晰。状态变化检测、定时采样、LCD局部刷新等模式在三个实验中反复出现,体现了嵌入式开发的通用范式。

遇到的问题

  • 光强传感器:串口显示470 lx但LCD显示210 lx,排查后发现是拍LCD照片时手机遮挡了传感器,属于操作层面的问题,也提醒了实验数据采集时要避免人为干扰。
  • 气体传感器:上电初期1-2分钟输出不稳定,这是半导体气敏传感器的预热特性,通过状态变化检测机制过滤掉了无效输出。另外还遇到了串口中文乱码问题,通过将源文件保存为UTF-8编码解决。
  • 光电开关:原代码直接打印数字0/1可读性差,修改为if-else判断输出中文"有遮挡/无遮挡",调试体验大幅提升。

五、扩展思考

应用场景拓展

  1. 智能照明系统:BH1750 + PWM调光LED,实现根据环境光自动调节室内亮度,节能效果显著。代码中已有LED控制函数,只需增加PWM输出和光强阈值判断即可实现闭环控制。

  2. 厨房燃气安全联动:MQ-5 + 继电器 + 燃气电磁阀,检测到泄漏时自动切断气源并联动排风扇。继电器控制只需调用HAL_GPIO_WritePin,与现有LED控制逻辑完全相同。

  3. 智慧车位检测:E18-D80NK朝下安装在每个车位上方,检测车辆是否停放。通过ESP8266 Wi-Fi模块上报到云平台,实现停车场车位引导。

  4. 流水线产品计数:光电开关安装在传送带两侧,产品经过时计数。检测到"无遮挡→有遮挡"的边沿时count++,还能通过两次遮挡间隔时间判断卡料异常。

安全与可靠性改进

  1. 传感器健康自诊断:MQ-5加热丝长期使用会老化,可通过ADC检测加热丝工作电流,偏差超过20%时提示维护。

  2. 多传感器融合:将光强、气体、光电开关三种传感器数据融合,实现更智能的环境监测——比如有人遮挡且光线变暗时自动开灯。

  3. 低功耗优化:利用STM32L4的STOP模式,RTC定时唤醒采样,将待机功耗降至微安级,支持电池长期运行。

更多推荐