STM32实战:基于STM32F103的智慧教室环境监控系统(CO₂+光照+人数统计)

文章目录

项目背景与系统概述

随着智慧校园建设的推进,教室环境质量对学生的学习效率和身体健康有着直接影响。二氧化碳浓度过高会导致学生注意力不集中、昏昏欲睡;光照强度不足或过强则会影响视力和学习舒适度;同时,统计教室人员数量可以为后勤管理、空调照明节能控制提供数据支撑。

本项目基于STM32F103C8T6微控制器,设计一套完整的智慧教室环境监控系统,实现对CO₂浓度、光照强度、教室人数的实时采集与显示,并配备数据异常报警功能。系统整体架构如下:
STM32F103C8T6

主控芯片
MH-Z19B

CO₂传感器
BH1750

光照传感器
HC-SR501×2

人体红外传感器
0.96寸OLED

显示模块
有源蜂鸣器

报警模块
ESP8266-01S

WiFi模块
阿里云IoT平台
手机APP/Web端

系统功能需求分析

本系统需要实现以下核心功能:

  1. CO₂浓度监测:通过MH-Z19B红外二氧化碳传感器实时采集教室内CO₂浓度,精度可达±50ppm
  2. 光照强度检测:利用BH1750数字光照传感器测量环境光照度,范围1-65535 lux
  3. 人数统计:采用两个HC-SR501人体红外传感器配合逻辑判断,实现人员进出检测
  4. 本地数据显示:通过0.96寸OLED屏幕实时显示各项参数
  5. 阈值报警:CO₂浓度超过设定阈值时,蜂鸣器发出声光报警
  6. 数据上云:通过ESP8266 WiFi模块将数据上传至阿里云IoT平台,实现远程监控

硬件选型与物料清单

序号 元器件名称 型号规格 数量 用途说明
1 主控芯片 STM32F103C8T6最小系统板 1 核心控制器
2 CO₂传感器 MH-Z19B 1 二氧化碳浓度检测
3 光照传感器 BH1750 GY-302模块 1 环境光照度检测
4 人体红外传感器 HC-SR501 2 人员进出检测
5 OLED显示屏 0.96寸 SSD1306 IIC接口 1 本地数据显示
6 WiFi模块 ESP8266-01S 1 无线数据传输
7 有源蜂鸣器 3.3V-5V有源蜂鸣器模块 1 报警提示
8 USB转TTL模块 CH340G 1 程序下载调试
9 面包板 830孔 1 电路搭建
10 杜邦线 公母/母母各一包 若干 电路连接
11 LED指示灯 5mm红色LED+220Ω电阻 2 状态指示

硬件接线详解

各模块引脚连接对应表

MH-Z19B CO₂传感器接线(UART通信):

MH-Z19B引脚 STM32引脚 说明
VCC(红色) 5V 传感器供电(需5V)
GND(黑色) GND 共地
TX(黄色) PA10(USART1-RX) 传感器发送→STM32接收
RX(绿色) PA9(USART1-TX) STM32发送→传感器接收

BH1750光照传感器接线(IIC通信):

BH1750引脚 STM32引脚 说明
VCC 3.3V 传感器供电
GND GND 共地
SCL PB6(I2C1-SCL) IIC时钟线
SDA PB7(I2C1-SDA) IIC数据线
ADDR GND 地址选择(接地为0x23)

HC-SR501人体红外传感器接线:

HC-SR501引脚(进门) STM32引脚 说明
VCC 5V 传感器供电
GND GND 共地
OUT PB0 进门检测信号
HC-SR501引脚(出门) STM32引脚 说明
VCC 5V 传感器供电
GND GND 共地
OUT PB1 出门检测信号

0.96寸OLED显示屏接线(IIC通信):

OLED引脚 STM32引脚 说明
VCC 3.3V 显示屏供电
GND GND 共地
SCL PB6(I2C1-SCL) IIC时钟线(与BH1750共用)
SDA PB7(I2C1-SDA) IIC数据线(与BH1750共用)

ESP8266-01S WiFi模块接线(UART通信):

ESP8266引脚 STM32引脚 说明
VCC 3.3V 模块供电(需独立供电,电流≥500mA)
GND GND 共地
TX PA3(USART2-RX) 模块发送→STM32接收
RX PA2(USART2-TX) STM32发送→模块接收
CH_PD/EN 3.3V 使能引脚,接高电平
RST 3.3V 复位引脚,接高电平

有源蜂鸣器模块接线:

蜂鸣器引脚 STM32引脚 说明
VCC 3.3V 蜂鸣器供电
GND GND 共地
I/O PC13 控制信号引脚

IIC总线共用说明

BH1750光照传感器和OLED显示屏共用同一组IIC总线(PB6-SCL, PB7-SDA),这是IIC总线的优势所在------支持多设备挂在同一总线上,通过不同的设备地址进行区分:

  • BH1750设备地址:0x23(ADDR引脚接地时)
  • SSD1306 OLED设备地址:0x3C

软件开发环境搭建

开发工具安装

  1. Keil MDK-ARM V5:STM32官方推荐的集成开发环境

  2. STM32CubeMX:图形化配置工具,用于生成初始化代码

  3. 串口调试助手:用于调试传感器和ESP8266通信

    • 推荐使用SSCOM或友善串口调试助手

STM32CubeMX工程配置步骤

打开STM32CubeMX,按照以下步骤进行配置:

步骤一:选择芯片型号

  • 点击"ACCESS TO MCU SELECTOR"
  • 在搜索框中输入"STM32F103C8T6"
  • 选中该芯片,点击"Start Project"

步骤二:配置系统时钟

  • 进入"Pinout & Configuration"页面
  • 在"System Core"中选择"RCC"
  • 将"HSE"设置为"Crystal/Ceramic Resonator"(外部晶振)
  • 在"Clock Configuration"页面中:
    • 选择HSE作为PLL输入源
    • 设置HCLK为72MHz(最大频率)
    • 设置APB1为36MHz,APB2为72MHz

步骤三:配置调试接口

  • 在"System Core"中选择"SYS"
  • 将"Debug"设置为"Serial Wire"(SWD调试接口)

步骤四:配置USART1(MH-Z19B CO₂传感器通信)

  • 在"Connectivity"中选择"USART1"
  • Mode设置为"Asynchronous"(异步通信)
  • 参数配置:
    • Baud Rate: 9600 bps
    • Word Length: 8 Bits
    • Parity: None
    • Stop Bits: 1

步骤五:配置USART2(ESP8266 WiFi通信)

  • 在"Connectivity"中选择"USART2"
  • Mode设置为"Asynchronous"
  • 参数配置:
    • Baud Rate: 115200 bps
    • Word Length: 8 Bits
    • Parity: None
    • Stop Bits: 1

步骤六:配置I2C1(BH1750和OLED共用)

  • 在"Connectivity"中选择"I2C1"
  • Mode选择"I2C"
  • 参数配置:
    • I2C Speed Mode: Standard Mode
    • Clock Speed: 100000 Hz(100KHz标准模式)
    • 其他保持默认

步骤七:配置GPIO引脚

  • PB0:设置为输入模式(GPIO_Input),标签命名为"PIR_ENTER",用于进门检测
  • PB1:设置为输入模式(GPIO_Input),标签命名为"PIR_EXIT",用于出门检测
  • PC13:设置为输出模式(GPIO_Output),标签命名为"BUZZER",用于蜂鸣器控制

步骤八:配置定时器

  • 在"Timers"中选择"TIM2"
  • 勾选"Internal Clock"作为时钟源
  • 参数配置:
    • Prescaler: 7200-1(7200分频)
    • Counter Period: 10000-1(计数周期)
    • 计算:72000000/(7200×10000)=1Hz,即1秒定时

步骤九:生成代码

  • 点击"Project Manager"选项卡
  • 设置项目名称:"SmartClassroom"
  • 设置项目路径
  • Toolchain/IDE选择"MDK-ARM V5"
  • 勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral"
  • 点击"GENERATE CODE"生成代码

KEIL工程创建与配置

  1. 打开Keil MDK-ARM V5
  2. 选择"Project" → "Open Project",找到STM32CubeMX生成的工程文件
  3. 如果提示安装器件包,根据提示安装对应的STM32F1系列支持包

传感器驱动代码编写

文件:mh_z19b.h

在工程目录的Inc文件夹中创建头文件mh_z19b.h

c 复制代码
/**
 * @file    mh_z19b.h
 * @brief   MH-Z19B CO₂传感器驱动头文件
 * @note    此文件定义了MH-Z19B传感器的相关宏定义和函数声明
 */

#ifndef __MH_Z19B_H
#define __MH_Z19B_H

#include "main.h"
#include "usart.h"

/* MH-Z19B命令定义 */
#define MHZ19B_CMD_READ_CO2         0x86    // 读取CO₂浓度命令
#define MHZ19B_CMD_CALIBRATE_ZERO   0x87    // 零点校准命令
#define MHZ19B_CMD_CALIBRATE_SPAN   0x88    // 跨度校准命令
#define MHZ19B_CMD_SET_AUTO_CALI    0x79    // 设置自动校准开关

/* 传感器读取状态 */
#define MHZ19B_STATUS_OK            0x00    // 读取成功
#define MHZ19B_STATUS_TIMEOUT       0x01    // 超时错误
#define MHZ19B_STATUS_CHECKSUM_ERR  0x02    // 校验和错误

/* 全局变量声明 - CO₂浓度值 */
extern uint16_t g_co2_concentration;

/* 函数声明 */
uint8_t MHZ19B_ReadCO2(void);
uint8_t MHZ19B_CalculateChecksum(uint8_t *pData, uint8_t len);
void MHZ19B_SendCommand(uint8_t cmd);
uint8_t MHZ19B_ReceiveResponse(uint8_t *pResponse, uint8_t expectedLen, uint32_t timeout);

#endif /* __MH_Z19B_H */

文件:mh_z19b.c

在工程目录的Src文件夹中创建源文件mh_z19b.c

c 复制代码
/**
 * @file    mh_z19b.c
 * @brief   MH-Z19B CO₂传感器驱动实现
 * @note    通过USART1与传感器通信,读取CO₂浓度值
 *          通信协议:9600bps, 8数据位, 无校验, 1停止位
 */

#include "mh_z19b.h"
#include <string.h>

/* 全局变量定义 */
uint16_t g_co2_concentration = 0;  // 存储CO₂浓度值,单位ppm

/* 用于接收传感器数据的缓冲区 */
static uint8_t g_uart1_rx_buffer[9];  // MH-Z19B响应数据固定为9字节
static uint8_t g_uart1_rx_index = 0;  // 接收缓冲区索引
static uint8_t g_uart1_rx_complete = 0; // 接收完成标志

/**
 * @brief  计算MH-Z19B数据包的校验和
 * @param  pData: 数据指针
 * @param  len: 数据长度(不包括校验和字节本身)
 * @return 校验和值(单字节,取反加1)
 * @note   MH-Z19B校验和算法:
 *         将除了校验和字节外的所有字节求和,取低8位,
 *         然后取反再加1
 */
uint8_t MHZ19B_CalculateChecksum(uint8_t *pData, uint8_t len)
{
    uint8_t i;
    uint16_t sum = 0;
    
    /* 累加所有字节 */
    for(i = 0; i < len; i++)
    {
        sum += pData[i];
    }
    
    /* 取低8位,取反后加1 */
    sum = sum & 0x00FF;
    sum = (~sum) + 1;
    
    return (uint8_t)sum;
}

/**
 * @brief  向MH-Z19B发送命令
 * @param  cmd: 命令字节
 * @note   发送格式:起始字节(0xFF) + 传感器编号(0x01) + 命令 + 数据区(5字节) + 校验和
 *         对于读取CO₂浓度的命令(0x86),数据区为:0x00 0x00 0x00 0x00 0x00
 */
void MHZ19B_SendCommand(uint8_t cmd)
{
    uint8_t tx_buffer[9];
    uint8_t checksum;
    
    /* 构建发送数据包 */
    tx_buffer[0] = 0xFF;  // 起始字节,固定值
    tx_buffer[1] = 0x01;  // 传感器编号,通常为0x01
    tx_buffer[2] = cmd;   // 命令字节
    
    /* 数据区填充(读取命令时数据区全为0) */
    tx_buffer[3] = 0x00;
    tx_buffer[4] = 0x00;
    tx_buffer[5] = 0x00;
    tx_buffer[6] = 0x00;
    tx_buffer[7] = 0x00;
    
    /* 计算校验和(对前8个字节计算) */
    checksum = MHZ19B_CalculateChecksum(tx_buffer, 8);
    tx_buffer[8] = checksum;
    
    /* 清空接收缓冲区 */
    g_uart1_rx_index = 0;
    g_uart1_rx_complete = 0;
    memset(g_uart1_rx_buffer, 0, sizeof(g_uart1_rx_buffer));
    
    /* 通过USART1发送数据 */
    HAL_UART_Transmit(&huart1, tx_buffer, 9, 1000);
}

/**
 * @brief  接收MH-Z19B的响应数据
 * @param  pResponse: 接收数据缓冲区指针
 * @param  expectedLen: 期望接收的数据长度
 * @param  timeout: 超时时间(毫秒)
 * @return 状态码:0-成功,1-超时,2-校验错误
 * @note   采用循环等待方式接收,实际项目中可改为中断接收
 */
uint8_t MHZ19B_ReceiveResponse(uint8_t *pResponse, uint8_t expectedLen, uint32_t timeout)
{
    uint8_t rx_byte;
    uint32_t start_time;
    uint8_t checksum;
    
    /* 等待接收完成或超时 */
    start_time = HAL_GetTick();
    g_uart1_rx_index = 0;
    
    while(g_uart1_rx_index < expectedLen)
    {
        /* 检查超时 */
        if((HAL_GetTick() - start_time) > timeout)
        {
            return MHZ19B_STATUS_TIMEOUT;
        }
        
        /* 检查是否有数据到达 */
        if(HAL_UART_Receive(&huart1, &rx_byte, 1, 10) == HAL_OK)
        {
            g_uart1_rx_buffer[g_uart1_rx_index] = rx_byte;
            g_uart1_rx_index++;
        }
    }
    
    /* 复制数据到输出缓冲区 */
    memcpy(pResponse, g_uart1_rx_buffer, expectedLen);
    
    /* 验证起始字节 */
    if(pResponse[0] != 0xFF)
    {
        return MHZ19B_STATUS_CHECKSUM_ERR;
    }
    
    /* 验证校验和 */
    checksum = MHZ19B_CalculateChecksum(pResponse, 8);
    if(checksum != pResponse[8])
    {
        return MHZ19B_STATUS_CHECKSUM_ERR;
    }
    
    return MHZ19B_STATUS_OK;
}

/**
 * @brief  读取CO₂浓度值
 * @return 状态码:0-成功,1-超时,2-校验错误
 * @note   成功读取后,CO₂浓度值存储在全局变量g_co2_concentration中
 *         计算公式:CO₂浓度 = 响应数据[2] * 256 + 响应数据[3]
 */
uint8_t MHZ19B_ReadCO2(void)
{
    uint8_t response[9];
    uint8_t status;
    
    /* 发送读取命令 */
    MHZ19B_SendCommand(MHZ19B_CMD_READ_CO2);
    
    /* 接收响应数据,超时时间设置为1000ms */
    status = MHZ19B_ReceiveResponse(response, 9, 1000);
    
    if(status == MHZ19B_STATUS_OK)
    {
        /* 解析CO₂浓度值 */
        /* 响应数据格式:
         * [0] 起始字节 0xFF
         * [1] 命令字节 0x86
         * [2] CO₂浓度高字节
         * [3] CO₂浓度低字节
         * [4]-[7] 其他数据
         * [8] 校验和
         */
        g_co2_concentration = ((uint16_t)response[2] << 8) | response[3];
    }
    
    return status;
}

/**
 * @brief  USART1接收完成回调函数(需在usart.c中配置中断)
 * @note   此函数为中断服务函数中调用的回调,
 *         如果使用中断方式接收,可启用此函数
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        /* 此处可添加USART1接收中断处理逻辑 */
        /* 本示例使用轮询方式,此函数暂不启用 */
    }
    else if(huart->Instance == USART2)
    {
        /* 此处可添加USART2接收中断处理逻辑 */
        /* ESP8266 WiFi模块通信使用 */
    }
}

文件:bh1750.h

在工程目录的Inc文件夹中创建头文件bh1750.h

c 复制代码
/**
 * @file    bh1750.h
 * @brief   BH1750光照传感器驱动头文件
 * @note    通过I2C1接口与传感器通信
 *          设备地址:ADDR接地时为0x23(7位地址),写地址0x46,读地址0x47
 */

#ifndef __BH1750_H
#define __BH1750_H

#include "main.h"
#include "i2c.h"

/* BH1750 I2C地址定义 */
#define BH1750_ADDR_WRITE   0x46    // 写地址(7位地址0x23左移1位,低位为0)
#define BH1750_ADDR_READ    0x47    // 读地址(7位地址0x23左移1位,低位为1)

/* BH1750指令定义 */
#define BH1750_CMD_POWER_ON         0x01    // 上电指令
#define BH1750_CMD_POWER_OFF        0x00    // 断电指令
#define BH1750_CMD_RESET            0x07    // 复位指令
#define BH1750_CMD_CONT_H_MODE      0x10    // 连续高分辨率模式(1lx精度,120ms测量时间)
#define BH1750_CMD_CONT_H_MODE2     0x11    // 连续高分辨率模式2(0.5lx精度,120ms测量时间)
#define BH1750_CMD_CONT_L_MODE      0x13    // 连续低分辨率模式(4lx精度,16ms测量时间)
#define BH1750_CMD_ONE_H_MODE       0x20    // 单次高分辨率模式

/* 全局变量声明 */
extern float g_light_intensity;  // 光照强度值,单位lux

/* 函数声明 */
void BH1750_Init(void);
uint8_t BH1750_SendCommand(uint8_t cmd);
uint8_t BH1750_ReadData(uint16_t *pLightValue);
float BH1750_GetLightIntensity(void);

#endif /* __BH1750_H */

文件:bh1750.c

在工程目录的Src文件夹中创建源文件bh1750.c

c 复制代码
/**
 * @file    bh1750.c
 * @brief   BH1750光照传感器驱动实现
 * @note    BH1750采用I2C通信协议,支持多种测量模式
 *          本驱动使用连续高分辨率模式,每120ms可获取一次数据
 */

#include "bh1750.h"

/* 全局变量定义 */
float g_light_intensity = 0.0f;  // 存储光照强度值,单位lux

/**
 * @brief  BH1750传感器初始化
 * @note   初始化步骤:上电 → 等待 → 设置测量模式
 *         必须先上电才能设置其他指令
 */
void BH1750_Init(void)
{
    /* 发送上电指令 */
    BH1750_SendCommand(BH1750_CMD_POWER_ON);
    
    /* 等待传感器稳定,至少需要1ms */
    HAL_Delay(10);
    
    /* 设置为连续高分辨率模式 */
    /* 此模式下测量精度为1lx,测量时间约120ms */
    BH1750_SendCommand(BH1750_CMD_CONT_H_MODE);
    
    /* 等待首次测量完成 */
    HAL_Delay(180);
}

/**
 * @brief  向BH1750发送单字节命令
 * @param  cmd: 命令字节
 * @return HAL状态
 * @note   BH1750的命令都是单字节的,通过I2C写入
 */
uint8_t BH1750_SendCommand(uint8_t cmd)
{
    HAL_StatusTypeDef status;
    
    /* 
     * 使用HAL库的I2C发送函数
     * 参数说明:
     *   &hi2c1    - I2C1句柄
     *   BH1750_ADDR_WRITE - 设备写地址(0x46)
     *   &cmd      - 待发送的命令字节指针
     *   1         - 发送数据长度(1字节)
     *   100       - 超时时间(毫秒)
     */
    status = HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDR_WRITE, &cmd, 1, 100);
    
    return (uint8_t)status;
}

/**
 * @brief  从BH1750读取光照数据
 * @param  pLightValue: 存放读取结果的指针
 * @return HAL状态
 * @note   BH1750返回2字节的原始光照数据
 *         计算公式:光照强度(lux) = 原始值 / 1.2
 *         连续高分辨率模式下,此公式已校准
 */
uint8_t BH1750_ReadData(uint16_t *pLightValue)
{
    uint8_t rx_buffer[2] = {0, 0};
    HAL_StatusTypeDef status;
    
    /* 
     * 从BH1750读取2字节数据
     * 参数说明:
     *   &hi2c1    - I2C1句柄
     *   BH1750_ADDR_READ - 设备读地址(0x47)
     *   rx_buffer - 接收缓冲区
     *   2         - 接收数据长度(2字节)
     *   200       - 超时时间(毫秒)
     */
    status = HAL_I2C_Master_Receive(&hi2c1, BH1750_ADDR_READ, rx_buffer, 2, 200);
    
    if(status == HAL_OK)
    {
        /* 合并高字节和低字节 */
        /* BH1750返回的是大端格式,高字节在前 */
        *pLightValue = ((uint16_t)rx_buffer[0] << 8) | rx_buffer[1];
    }
    
    return (uint8_t)status;
}

/**
 * @brief  获取光照强度值(lux)
 * @return 光照强度,单位lux
 * @note   返回的值为经过公式换算后的实际光照强度
 *         如果读取失败,返回-1.0表示错误
 */
float BH1750_GetLightIntensity(void)
{
    uint16_t raw_value = 0;
    uint8_t status;
    float lux = 0.0f;
    
    /* 读取原始数据 */
    status = BH1750_ReadData(&raw_value);
    
    if(status == HAL_OK)
    {
        /* 
         * 将原始值转换为光照强度(lux)
         * 公式:lux = raw_value / 1.2
         * 这是BH1750数据手册中的标准换算公式
         * 在高分辨率模式下,此公式精确可靠
         */
        lux = (float)raw_value / 1.2f;
        g_light_intensity = lux;
        return lux;
    }
    else
    {
        /* 读取失败返回-1.0作为错误标志 */
        return -1.0f;
    }
}

文件:people_counter.h

在工程目录的Inc文件夹中创建头文件people_counter.h

c 复制代码
/**
 * @file    people_counter.h
 * @brief   教室人数统计模块头文件
 * @note    使用两个HC-SR501人体红外传感器实现人员进出检测
 *          逻辑判断:先触发进门传感器→后触发出门传感器 = 有人进入
 *                   先触发出门传感器→后触发进门传感器 = 有人离开
 */

#ifndef __PEOPLE_COUNTER_H
#define __PEOPLE_COUNTER_H

#include "main.h"
#include "gpio.h"

/* 传感器状态定义 */
#define PIR_NO_TRIGGER      0   // 无触发
#define PIR_TRIGGERED       1   // 已触发

/* 触发时间窗口(毫秒) */
#define TRIGGER_TIMEOUT     3000  // 两个传感器之间允许的最大触发间隔

/* 全局变量声明 */
extern uint16_t g_people_count;  // 当前教室内人数

/* 函数声明 */
void PeopleCounter_Init(void);
void PeopleCounter_Scan(void);
uint8_t PeopleCounter_GetPIREnterState(void);
uint8_t PeopleCounter_GetPIRExitState(void);
void PeopleCounter_Reset(void);

#endif /* __PEOPLE_COUNTER_H */

文件:people_counter.c

在工程目录的Src文件夹中创建源文件people_counter.c

c 复制代码
/**
 * @file    people_counter.c
 * @brief   教室人数统计模块实现
 * @note    核心算法说明:
 *          1. 进门传感器(PB0)和出门传感器(PB1)分别安装在门口内外两侧
 *          2. 当有人进入教室时,先触发进门传感器,再触发出门传感器
 *          3. 当有人离开教室时,先触发出门传感器,再触发进门传感器
 *          4. 两个传感器触发间隔在设定的时间窗口内,计为一次有效的进出
 *          5. 人数最小值为0,不会出现负数
 */

#include "people_counter.h"

/* 全局变量定义 */
uint16_t g_people_count = 0;  // 教室内人数,初始化为0

/* 内部状态变量 */
static uint8_t s_pir_enter_triggered = PIR_NO_TRIGGER;  // 进门传感器触发标志
static uint8_t s_pir_exit_triggered = PIR_NO_TRIGGER;   // 出门传感器触发标志
static uint32_t s_pir_enter_trigger_time = 0;           // 进门传感器触发时间戳
static uint32_t s_pir_exit_trigger_time = 0;            // 出门传感器触发时间戳

/* 传感器状态跟踪(用于检测边沿变化) */
static uint8_t s_last_enter_state = 0;  // 进门传感器上一次状态
static uint8_t s_last_exit_state = 0;   // 出门传感器上一次状态

/**
 * @brief  人数统计模块初始化
 * @note   初始化计数器为0,清除所有触发标志
 */
void PeopleCounter_Init(void)
{
    g_people_count = 0;
    s_pir_enter_triggered = PIR_NO_TRIGGER;
    s_pir_exit_triggered = PIR_NO_TRIGGER;
    s_pir_enter_trigger_time = 0;
    s_pir_exit_trigger_time = 0;
    s_last_enter_state = 0;
    s_last_exit_state = 0;
}

/**
 * @brief  获取进门传感器当前状态
 * @return 传感器电平状态:0-无触发,1-有触发
 * @note   HC-SR501传感器输出高电平表示检测到人体活动
 */
uint8_t PeopleCounter_GetPIREnterState(void)
{
    /* 读取PB0引脚电平状态 */
    return (uint8_t)HAL_GPIO_ReadPin(PIR_ENTER_GPIO_Port, PIR_ENTER_Pin);
}

/**
 * @brief  获取出门传感器当前状态
 * @return 传感器电平状态:0-无触发,1-有触发
 * @note   HC-SR501传感器输出高电平表示检测到人体活动
 */
uint8_t PeopleCounter_GetPIRExitState(void)
{
    /* 读取PB1引脚电平状态 */
    return (uint8_t)HAL_GPIO_ReadPin(PIR_EXIT_GPIO_Port, PIR_EXIT_Pin);
}

/**
 * @brief  重置所有触发状态
 * @note   在完成一次进出判断后调用,避免重复计数
 */
void PeopleCounter_Reset(void)
{
    s_pir_enter_triggered = PIR_NO_TRIGGER;
    s_pir_exit_triggered = PIR_NO_TRIGGER;
    s_pir_enter_trigger_time = 0;
    s_pir_exit_trigger_time = 0;
}

/**
 * @brief  人员进出检测主函数
 * @note   此函数需要在主循环中周期性调用,建议每100ms调用一次
 *         检测逻辑采用边沿触发判断,避免持续高电平导致重复计数
 *         核心判断流程如下:
 */
void PeopleCounter_Scan(void)
{
    uint8_t current_enter_state;
    uint8_t current_exit_state;
    uint32_t current_time;
    uint32_t time_diff;
    
    /* 获取当前时间和传感器状态 */
    current_time = HAL_GetTick();
    current_enter_state = PeopleCounter_GetPIREnterState();
    current_exit_state = PeopleCounter_GetPIRExitState();
    
    /*
     * 判断进门传感器是否有上升沿变化
     * 当上一次为低电平,当前为高电平时,表示检测到人员
     */
    if(s_last_enter_state == 0 && current_enter_state == 1)
    {
        /* 进门传感器被触发 */
        s_pir_enter_triggered = PIR_TRIGGERED;
        s_pir_enter_trigger_time = current_time;
        
        /*
         * 检查出门传感器是否在时间窗口内已被触发
         * 如果是,说明有人先经过了出门传感器,即有人离开
         */
        if(s_pir_exit_triggered == PIR_TRIGGERED)
        {
            time_diff = current_time - s_pir_exit_trigger_time;
            if(time_diff <= TRIGGER_TIMEOUT)
            {
                /* 时间窗口内的连续触发,判断为有人离开 */
                if(g_people_count > 0)
                {
                    g_people_count--;  // 人数减1
                }
                PeopleCounter_Reset();  // 重置状态,准备下一次检测
            }
        }
    }
    
    /*
     * 判断出门传感器是否有上升沿变化
     * 当上一次为低电平,当前为高电平时,表示检测到人员
     */
    if(s_last_exit_state == 0 && current_exit_state == 1)
    {
        /* 出门传感器被触发 */
        s_pir_exit_triggered = PIR_TRIGGERED;
        s_pir_exit_trigger_time = current_time;
        
        /*
         * 检查进门传感器是否在时间窗口内已被触发
         * 如果是,说明有人先经过了进门传感器,即有人进入
         */
        if(s_pir_enter_triggered == PIR_TRIGGERED)
        {
            time_diff = current_time - s_pir_enter_trigger_time;
            if(time_diff <= TRIGGER_TIMEOUT)
            {
                /* 时间窗口内的连续触发,判断为有人进入 */
                g_people_count++;  // 人数加1
                PeopleCounter_Reset();  // 重置状态,准备下一次检测
            }
        }
    }
    
    /*
     * 检查触发超时
     * 如果某个传感器被触发但超过时间窗口没有另一个传感器触发
     * 则重置该触发状态(可能是误触发或单人快速通过的情况)
     */
    if(s_pir_enter_triggered == PIR_TRIGGERED)
    {
        if((current_time - s_pir_enter_trigger_time) > TRIGGER_TIMEOUT)
        {
            s_pir_enter_triggered = PIR_NO_TRIGGER;
        }
    }
    
    if(s_pir_exit_triggered == PIR_TRIGGERED)
    {
        if((current_time - s_pir_exit_trigger_time) > TRIGGER_TIMEOUT)
        {
            s_pir_exit_triggered = PIR_NO_TRIGGER;
        }
    }
    
    /* 保存当前状态,用于下一次边沿检测 */
    s_last_enter_state = current_enter_state;
    s_last_exit_state = current_exit_state;
}

人数检测的算法流程图如下:














开始扫描
进门传感器

触发?
记录进门触发时间
出门传感器

触发?
出门传感器

已触发?
时间差在

窗口内?
人数减1

重置状态
仅记录进门触发
记录出门触发时间
检查触发

超时?
进门传感器

已触发?
时间差在

窗口内?
人数加1

重置状态
仅记录出门触发
清除超时

触发记录
更新传感器

状态记录
返回,等待

下次扫描

文件:oled_display.h

在工程目录的Inc文件夹中创建头文件oled_display.h

c 复制代码
/**
 * @file    oled_display.h
 * @brief   0.96寸OLED显示屏驱动头文件(SSD1306控制器)
 * @note    I2C接口,地址0x3C
 *          分辨率128×64像素
 */

#ifndef __OLED_DISPLAY_H
#define __OLED_DISPLAY_H

#include "main.h"
#include "i2c.h"
#include <stdarg.h>
#include <stdio.h>

/* SSD1306 I2C地址 */
#define SSD1306_I2C_ADDR    0x78  // 0x3C左移1位,即0x78

/* SSD1306分辨率 */
#define SSD1306_WIDTH       128
#define SSD1306_HEIGHT      64
#define SSD1306_PAGES       8     // 64/8=8页

/* 函数声明 */
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ClearArea(uint8_t x, uint8_t y, uint8_t width, uint8_t height);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen, uint8_t decLen, uint8_t size);
void OLED_Printf(uint8_t x, uint8_t y, uint8_t size, const char *fmt, ...);
void OLED_Refresh(void);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color);
void OLED_UpdateScreen(void);

#endif /* __OLED_DISPLAY_H */

文件:oled_display.c

在工程目录的Src文件夹中创建源文件oled_display.c

c 复制代码
/**
 * @file    oled_display.c
 * @brief   0.96寸OLED显示屏驱动实现
 * @note    SSD1306驱动,I2C通信,支持6×8和8×16两种字体
 *          显示缓冲区存储在RAM中,通过OLED_UpdateScreen刷新到屏幕
 *          本驱动实现了基本的字符、字符串、数字显示功能
 */

#include "oled_display.h"
#include "oled_font.h"  // 字库文件,需自行准备或使用内置字库
#include <string.h>
#include <math.h>

/* 显示缓冲区,128×64像素对应128×8字节 */
static uint8_t s_oled_buffer[SSD1306_WIDTH * SSD1306_PAGES];

/* 6×8 ASCII字库(部分字符,完整字库请参考font.h) */
static const uint8_t Font6x8[][6] = {
    {0x00,0x00,0x00,0x00,0x00,0x00}, // 空格 (0x20)
    {0x00,0x00,0x5F,0x00,0x00,0x00}, // !
    {0x00,0x07,0x00,0x07,0x00,0x00}, // "
    {0x14,0x7F,0x14,0x7F,0x14,0x00}, // #
    {0x24,0x2A,0x7F,0x2A,0x12,0x00}, // $
    {0x23,0x13,0x08,0x64,0x62,0x00}, // %
    {0x36,0x49,0x55,0x22,0x50,0x00}, // &
    {0x00,0x05,0x03,0x00,0x00,0x00}, // '
    {0x00,0x1C,0x22,0x41,0x00,0x00}, // (
    {0x00,0x41,0x22,0x1C,0x00,0x00}, // )
    {0x08,0x2A,0x1C,0x2A,0x08,0x00}, // *
    {0x08,0x08,0x3E,0x08,0x08,0x00}, // +
    {0x00,0x50,0x30,0x00,0x00,0x00}, // ,
    {0x08,0x08,0x08,0x08,0x08,0x00}, // -
    {0x00,0x60,0x60,0x00,0x00,0x00}, // .
    {0x20,0x10,0x08,0x04,0x02,0x00}, // /
    {0x3E,0x51,0x49,0x45,0x3E,0x00}, // 0
    {0x00,0x42,0x7F,0x40,0x00,0x00}, // 1
    {0x42,0x61,0x51,0x49,0x46,0x00}, // 2
    {0x21,0x41,0x45,0x4B,0x31,0x00}, // 3
    {0x18,0x14,0x12,0x7F,0x10,0x00}, // 4
    {0x27,0x45,0x45,0x45,0x39,0x00}, // 5
    {0x3C,0x4A,0x49,0x49,0x30,0x00}, // 6
    {0x01,0x71,0x09,0x05,0x03,0x00}, // 7
    {0x36,0x49,0x49,0x49,0x36,0x00}, // 8
    {0x06,0x49,0x49,0x29,0x1E,0x00}, // 9
    {0x00,0x36,0x36,0x00,0x00,0x00}, // :
    {0x00,0x56,0x36,0x00,0x00,0x00}, // ;
    {0x00,0x08,0x14,0x22,0x41,0x00}, // <
    {0x14,0x14,0x14,0x14,0x14,0x00}, // =
    {0x41,0x22,0x14,0x08,0x00,0x00}, // >
    {0x02,0x01,0x51,0x09,0x06,0x00}, // ?
    {0x32,0x49,0x79,0x41,0x3E,0x00}, // @
    {0x7E,0x11,0x11,0x11,0x7E,0x00}, // A
    {0x7F,0x49,0x49,0x49,0x36,0x00}, // B
    {0x3E,0x41,0x41,0x41,0x22,0x00}, // C
    {0x7F,0x41,0x41,0x22,0x1C,0x00}, // D
    {0x7F,0x49,0x49,0x49,0x41,0x00}, // E
    {0x7F,0x09,0x09,0x01,0x01,0x00}, // F
    {0x3E,0x41,0x41,0x51,0x32,0x00}, // G
    {0x7F,0x08,0x08,0x08,0x7F,0x00}, // H
    {0x00,0x41,0x7F,0x41,0x00,0x00}, // I
    {0x20,0x40,0x41,0x3F,0x01,0x00}, // J
    {0x7F,0x08,0x14,0x22,0x41,0x00}, // K
    {0x7F,0x40,0x40,0x40,0x40,0x00}, // L
    {0x7F,0x02,0x04,0x02,0x7F,0x00}, // M
    {0x7F,0x04,0x08,0x10,0x7F,0x00}, // N
    {0x3E,0x41,0x41,0x41,0x3E,0x00}, // O
    {0x7F,0x09,0x09,0x09,0x06,0x00}, // P
    {0x3E,0x41,0x51,0x21,0x5E,0x00}, // Q
    {0x7F,0x09,0x19,0x29,0x46,0x00}, // R
    {0x46,0x49,0x49,0x49,0x31,0x00}, // S
    {0x01,0x01,0x7F,0x01,0x01,0x00}, // T
    {0x3F,0x40,0x40,0x40,0x3F,0x00}, // U
    {0x1F,0x20,0x40,0x20,0x1F,0x00}, // V
    {0x7F,0x20,0x18,0x20,0x7F,0x00}, // W
    {0x63,0x14,0x08,0x14,0x63,0x00}, // X
    {0x03,0x04,0x78,0x04,0x03,0x00}, // Y
    {0x61,0x51,0x49,0x45,0x43,0x00}, // Z
};

/* 8×16 ASCII字库前32个字符 */
static const uint8_t Font8x16[][16] = {
    // 空格 0x20
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    // ! 0x21
    {0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00},
    // " 0x22
    {0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    // 以下省略其他字符,完整字库请参考标准8×16 ASCII字库
    // 实际使用时需要完整实现95个可打印ASCII字符
};

/**
 * @brief  向SSD1306发送命令
 * @param  cmd: 命令字节
 */
static void OLED_WriteCommand(uint8_t cmd)
{
    uint8_t data[2];
    data[0] = 0x00;  // 控制字节:0x00表示后续是命令
    data[1] = cmd;
    HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, data, 2, 100);
}

/**
 * @brief  向SSD1306发送数据
 * @param  dat: 数据字节
 */
static void OLED_WriteData(uint8_t dat)
{
    uint8_t data[2];
    data[0] = 0x40;  // 控制字节:0x40表示后续是数据
    data[1] = dat;
    HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, data, 2, 100);
}

/**
 * @brief  OLED初始化
 * @note   按照SSD1306数据手册的初始化序列配置
 */
void OLED_Init(void)
{
    /* 等待OLED上电稳定 */
    HAL_Delay(100);
    
    /* SSD1306初始化序列 */
    OLED_WriteCommand(0xAE);  // 关闭显示
    
    OLED_WriteCommand(0xD5);  // 设置显示时钟分频/振荡器频率
    OLED_WriteCommand(0x80);  // 默认值
    
    OLED_WriteCommand(0xA8);  // 设置多路复用率
    OLED_WriteCommand(0x3F);  // 64路复用
    
    OLED_WriteCommand(0xD3);  // 设置显示偏移
    OLED_WriteCommand(0x00);  // 无偏移
    
    OLED_WriteCommand(0x40);  // 设置显示起始行
    
    OLED_WriteCommand(0x8D);  // 充电泵设置
    OLED_WriteCommand(0x14);  // 启用充电泵
    
    OLED_WriteCommand(0x20);  // 设置内存地址模式
    OLED_WriteCommand(0x00);  // 水平寻址模式
    
    OLED_WriteCommand(0xA1);  // 段重映射,列127映射到SEG0
    
    OLED_WriteCommand(0xC8);  // COM扫描方向,从COM[N-1]扫描到COM0
    
    OLED_WriteCommand(0xDA);  // 设置COM引脚硬件配置
    OLED_WriteCommand(0x12);  // 备选COM引脚配置
    
    OLED_WriteCommand(0x81);  // 设置对比度
    OLED_WriteCommand(0xCF);  // 对比度值
    
    OLED_WriteCommand(0xD9);  // 设置预充电周期
    OLED_WriteCommand(0xF1);  // 相位1:1 DCLK, 相位2:15 DCLKs
    
    OLED_WriteCommand(0xDB);  // 设置VCOMH取消选择级别
    OLED_WriteCommand(0x40);  // ~0.77×VCC
    
    OLED_WriteCommand(0xA4);  // 恢复显示(非全亮)
    
    OLED_WriteCommand(0xA6);  // 正常显示(非反白)
    
    OLED_WriteCommand(0x2E);  // 停用滚动
    
    OLED_WriteCommand(0xAF);  // 开启显示
    
    /* 清空显示缓冲区 */
    OLED_Clear();
    OLED_UpdateScreen();
}

/**
 * @brief  清空显示缓冲区
 */
void OLED_Clear(void)
{
    memset(s_oled_buffer, 0x00, sizeof(s_oled_buffer));
}

/**
 * @brief  将缓冲区数据刷新到OLED屏幕
 */
void OLED_UpdateScreen(void)
{
    uint8_t page, seg;
    
    for(page = 0; page < SSD1306_PAGES; page++)
    {
        /* 设置页地址 */
        OLED_WriteCommand(0xB0 + page);
        /* 设置列地址低4位 */
        OLED_WriteCommand(0x00);
        /* 设置列地址高4位 */
        OLED_WriteCommand(0x10);
        
        /* 发送该页的所有列数据 */
        for(seg = 0; seg < SSD1306_WIDTH; seg++)
        {
            OLED_WriteData(s_oled_buffer[page * SSD1306_WIDTH + seg]);
        }
    }
}

/**
 * @brief  在缓冲区中绘制一个像素点
 * @param  x: 横坐标(0-127)
 * @param  y: 纵坐标(0-63)
 * @param  color: 0-熄灭,1-点亮
 */
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color)
{
    uint8_t page;
    uint8_t bit;
    
    if(x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT)
        return;
    
    page = y / 8;       // 确定像素所在页
    bit = y % 8;        // 确定像素在页内的位
    
    if(color)
    {
        s_oled_buffer[page * SSD1306_WIDTH + x] |= (1 << bit);
    }
    else
    {
        s_oled_buffer[page * SSD1306_WIDTH + x] &= ~(1 << bit);
    }
}

/**
 * @brief  在指定位置显示一个6×8字符
 * @param  x: 横坐标(0-127)
 * @param  y: 纵坐标(0-63)
 * @param  ch: 要显示的字符
 * @param  size: 字体大小,12表示6×8,16表示8×16
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size)
{
    uint8_t i, j;
    uint8_t temp;
    uint8_t char_index;
    
    /* 计算字符在字库中的索引(以空格为基准) */
    char_index = ch - 0x20;
    
    if(size == 12)  // 6×8字体
    {
        if(x + 6 > SSD1306_WIDTH || y + 8 > SSD1306_HEIGHT)
            return;
        
        for(i = 0; i < 6; i++)
        {
            temp = Font6x8[char_index][i];
            for(j = 0; j < 8; j++)
            {
                if(temp & (0x01 << j))
                {
                    OLED_DrawPoint(x + i, y + j, 1);
                }
                else
                {
                    OLED_DrawPoint(x + i, y + j, 0);
                }
            }
        }
    }
    else if(size == 16)  // 8×16字体
    {
        if(x + 8 > SSD1306_WIDTH || y + 16 > SSD1306_HEIGHT)
            return;
        
        for(i = 0; i < 8; i++)
        {
            temp = Font8x16[char_index][i];
            for(j = 0; j < 8; j++)
            {
                if(temp & (0x01 << j))
                {
                    OLED_DrawPoint(x + i, y + j, 1);
                }
                else
                {
                    OLED_DrawPoint(x + i, y + j, 0);
                }
            }
        }
        for(i = 0; i < 8; i++)
        {
            temp = Font8x16[char_index][i + 8];
            for(j = 0; j < 8; j++)
            {
                if(temp & (0x01 << j))
                {
                    OLED_DrawPoint(x + i, y + 8 + j, 1);
                }
                else
                {
                    OLED_DrawPoint(x + i, y + 8 + j, 0);
                }
            }
        }
    }
}

/**
 * @brief  在指定位置显示字符串
 * @param  x: 起始横坐标
 * @param  y: 起始纵坐标
 * @param  str: 字符串指针
 * @param  size: 字体大小(12或16)
 */
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size)
{
    uint8_t char_width;
    
    if(size == 12)
        char_width = 6;
    else if(size == 16)
        char_width = 8;
    else
        return;
    
    while(*str != '\0')
    {
        if(x > SSD1306_WIDTH - char_width)
        {
            x = 0;
            y += size;
        }
        if(y > SSD1306_HEIGHT - size)
            break;
        
        OLED_ShowChar(x, y, *str, size);
        x += char_width;
        str++;
    }
}

/**
 * @brief  在指定位置显示数字
 * @param  x: 起始横坐标
 * @param  y: 起始纵坐标
 * @param  num: 要显示的数字
 * @param  len: 数字位数
 * @param  size: 字体大小
 */
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
    char str[12];
    snprintf(str, sizeof(str), "%*lu", len, (unsigned long)num);
    OLED_ShowString(x, y, str, size);
}

/**
 * @brief  在指定位置显示浮点数
 * @param  x: 起始横坐标
 * @param  y: 起始纵坐标
 * @param  num: 浮点数
 * @param  intLen: 整数部分位数
 * @param  decLen: 小数部分位数
 * @param  size: 字体大小
 */
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen, uint8_t decLen, uint8_t size)
{
    char str[16];
    snprintf(str, sizeof(str), "%*.*f", intLen + decLen + 1, decLen, num);
    OLED_ShowString(x, y, str, size);
}

/**
 * @brief  格式化字符串显示(类似printf)
 * @param  x: 起始横坐标
 * @param  y: 起始纵坐标
 * @param  size: 字体大小
 * @param  fmt: 格式化字符串
 */
void OLED_Printf(uint8_t x, uint8_t y, uint8_t size, const char *fmt, ...)
{
    char buffer[64];
    va_list args;
    
    va_start(args, fmt);
    vsnprintf(buffer, sizeof(buffer), fmt, args);
    va_end(args);
    
    OLED_ShowString(x, y, buffer, size);
}

文件:wifi_esp8266.h

在工程目录的Inc文件夹中创建头文件wifi_esp8266.h

c 复制代码
/**
 * @file    wifi_esp8266.h
 * @brief   ESP8266-01S WiFi模块驱动头文件
 * @note    通过USART2与ESP8266通信
 *          支持AT指令集,实现MQTT数据上传
 */

#ifndef __WIFI_ESP8266_H
#define __WIFI_ESP8266_H

#include "main.h"
#include "usart.h"
#include <string.h>
#include <stdio.h>

/* WiFi配置信息 */
#define WIFI_SSID           "YourWiFiSSID"      // WiFi名称,需修改为实际值
#define WIFI_PASSWORD       "YourWiFiPassword"  // WiFi密码,需修改为实际值

/* MQTT服务器配置 */
#define MQTT_SERVER_IP      "your-server-ip"    // MQTT服务器地址
#define MQTT_SERVER_PORT    1883                 // MQTT服务器端口
#define MQTT_CLIENT_ID      "STM32_Classroom"   // MQTT客户端ID
#define MQTT_TOPIC_CO2      "/classroom/co2"     // CO₂数据上报主题
#define MQTT_TOPIC_LIGHT    "/classroom/light"   // 光照数据上报主题
#define MQTT_TOPIC_PEOPLE   "/classroom/people"  // 人数数据上报主题

/* ESP8266状态 */
#define ESP8266_OK          0x00
#define ESP8266_ERROR       0x01
#define ESP8266_TIMEOUT     0x02

/* 函数声明 */
uint8_t ESP8266_Init(void);
uint8_t ESP8266_ConnectWiFi(void);
uint8_t ESP8266_SendATCommand(const char *cmd, const char *expectResponse, uint32_t timeout);
uint8_t ESP8266_MQTT_Connect(void);
uint8_t ESP8266_MQTT_Publish(const char *topic, const char *payload);
void ESP8266_ClearBuffer(void);

#endif /* __WIFI_ESP8266_H */

文件:wifi_esp8266.c

在工程目录的Src文件夹中创建源文件wifi_esp8266.c

c 复制代码
/**
 * @file    wifi_esp8266.c
 * @brief   ESP8266-01S WiFi模块驱动实现
 * @note    AT指令集操作,实现WiFi连接和MQTT数据上报
 *          通信波特率115200bps
 */

#include "wifi_esp8266.h"

/* 接收缓冲区 */
static uint8_t g_esp8266_rx_buffer[512];
static uint16_t g_esp8266_rx_len = 0;

/**
 * @brief  清空ESP8266接收缓冲区
 */
void ESP8266_ClearBuffer(void)
{
    memset(g_esp8266_rx_buffer, 0, sizeof(g_esp8266_rx_buffer));
    g_esp8266_rx_len = 0;
}

/**
 * @brief  向ESP8266发送AT指令并等待响应
 * @param  cmd: AT指令字符串
 * @param  expectResponse: 期望的响应关键字
 * @param  timeout: 超时时间(毫秒)
 * @return 状态码
 * @note   发送AT指令后等待模块响应,检查是否包含期望的关键字
 */
uint8_t ESP8266_SendATCommand(const char *cmd, const char *expectResponse, uint32_t timeout)
{
    uint32_t start_time;
    uint8_t rx_byte;
    
    /* 清空接收缓冲区 */
    ESP8266_ClearBuffer();
    
    /* 发送AT指令,需要添加回车换行 */
    char cmd_with_rn[256];
    snprintf(cmd_with_rn, sizeof(cmd_with_rn), "%s\r\n", cmd);
    HAL_UART_Transmit(&huart2, (uint8_t *)cmd_with_rn, strlen(cmd_with_rn), 1000);
    
    /* 等待并接收响应 */
    start_time = HAL_GetTick();
    while((HAL_GetTick() - start_time) < timeout)
    {
        if(HAL_UART_Receive(&huart2, &rx_byte, 1, 10) == HAL_OK)
        {
            if(g_esp8266_rx_len < sizeof(g_esp8266_rx_buffer) - 1)
            {
                g_esp8266_rx_buffer[g_esp8266_rx_len++] = rx_byte;
                g_esp8266_rx_buffer[g_esp8266_rx_len] = '\0';
                
                /* 检查是否收到期望的响应 */
                if(strstr((char *)g_esp8266_rx_buffer, expectResponse) != NULL)
                {
                    return ESP8266_OK;
                }
                
                /* 检查是否有错误 */
                if(strstr((char *)g_esp8266_rx_buffer, "ERROR") != NULL)
                {
                    return ESP8266_ERROR;
                }
            }
        }
    }
    
    return ESP8266_TIMEOUT;
}

/**
 * @brief  ESP8266模块初始化
 * @return 状态码
 * @note   测试AT通信是否正常
 */
uint8_t ESP8266_Init(void)
{
    uint8_t retry = 0;
    uint8_t result;
    
    /* 发送AT测试指令,检查模块是否正常 */
    while(retry < 5)
    {
        result = ESP8266_SendATCommand("AT", "OK", 2000);
        if(result == ESP8266_OK)
        {
            break;
        }
        retry++;
        HAL_Delay(500);
    }
    
    if(result != ESP8266_OK)
    {
        return ESP8266_ERROR;
    }
    
    /* 设置工作模式为Station模式 */
    ESP8266_SendATCommand("AT+CWMODE=1", "OK", 2000);
    
    /* 关闭回显 */
    ESP8266_SendATCommand("ATE0", "OK", 2000);
    
    return ESP8266_OK;
}

/**
 * @brief  连接WiFi网络
 * @return 状态码
 * @note   需要修改WIFI_SSID和WIFI_PASSWORD宏定义为实际值
 */
uint8_t ESP8266_ConnectWiFi(void)
{
    uint8_t result;
    char cmd[128];
    uint32_t start_time;
    
    /* 构建连接WiFi的AT指令 */
    snprintf(cmd, sizeof(cmd), "AT+CWJAP=\"%s\",\"%s\"", WIFI_SSID, WIFI_PASSWORD);
    
    /* 发送连接指令,WiFi连接可能需要较长时间 */
    result = ESP8266_SendATCommand(cmd, "WIFI GOT IP", 15000);
    
    if(result != ESP8266_OK)
    {
        return ESP8266_ERROR;
    }
    
    /* 等待获取IP地址 */
    HAL_Delay(2000);
    
    return ESP8266_OK;
}

/**
 * @brief  连接MQTT服务器
 * @return 状态码
 * @note   使用AT+MQTTUSERCFG和AT+MQTTCONN指令
 *         本示例使用ESP8266的MQTT AT指令
 *         实际使用时可能需要固件支持MQTT AT指令集
 */
uint8_t ESP8266_MQTT_Connect(void)
{
    char cmd[256];
    uint8_t result;
    
    /* 配置MQTT用户信息 */
    snprintf(cmd, sizeof(cmd), "AT+MQTTUSERCFG=0,1,\"%s\",\"\",\"\",0,0,\"\"", MQTT_CLIENT_ID);
    result = ESP8266_SendATCommand(cmd, "OK", 3000);
    if(result != ESP8266_OK) return ESP8266_ERROR;
    
    /* 连接到MQTT服务器 */
    snprintf(cmd, sizeof(cmd), "AT+MQTTCONN=0,\"%s\",%d,0", MQTT_SERVER_IP, MQTT_SERVER_PORT);
    result = ESP8266_SendATCommand(cmd, "OK", 10000);
    if(result != ESP8266_OK) return ESP8266_ERROR;
    
    return ESP8266_OK;
}

/**
 * @brief  发布MQTT消息
 * @param  topic: 主题名称
 * @param  payload: 消息内容
 * @return 状态码
 */
uint8_t ESP8266_MQTT_Publish(const char *topic, const char *payload)
{
    char cmd[512];
    uint8_t result;
    
    /* 构建MQTT发布指令 */
    snprintf(cmd, sizeof(cmd), "AT+MQTTPUB=0,\"%s\",\"%s\",0,0", topic, payload);
    result = ESP8266_SendATCommand(cmd, "OK", 5000);
    
    return result;
}

主程序设计

文件:main.c(修改原有的main.c)

将STM32CubeMX生成的main.c按照以下内容进行修改:

c 复制代码
/**
 * @file    main.c
 * @brief   智慧教室环境监控系统主程序
 * @note    系统功能:
 *          1. 每2秒采集一次CO₂浓度(MH-Z19B预热后稳定)
 *          2. 每500ms采集一次光照强度
 *          3. 每100ms扫描一次人体红外传感器,进行人数统计
 *          4. 每1秒刷新一次OLED显示
 *          5. CO₂浓度超过1500ppm时触发蜂鸣器报警
 *          6. 每10秒通过WiFi上传一次数据到云端
 */

#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "i2c.h"
#include "tim.h"
#include "mh_z19b.h"
#include "bh1750.h"
#include "people_counter.h"
#include "oled_display.h"
#include "wifi_esp8266.h"
#include <stdio.h>
#include <string.h>

/* 系统参数宏定义 */
#define CO2_ALARM_THRESHOLD      1500    // CO₂报警阈值(ppm),超过此值触发报警
#define CO2_SAMPLE_INTERVAL      2000    // CO₂采样间隔(ms)
#define LIGHT_SAMPLE_INTERVAL    500     // 光照采样间隔(ms)
#define PIR_SCAN_INTERVAL        100     // 人体红外扫描间隔(ms)
#define DISPLAY_UPDATE_INTERVAL  1000    // 显示刷新间隔(ms)
#define WIFI_UPLOAD_INTERVAL     10000   // WiFi数据上传间隔(ms)

/* 系统状态变量 */
static uint32_t s_last_co2_sample_time = 0;     // 上次CO₂采样时间
static uint32_t s_last_light_sample_time = 0;   // 上次光照采样时间
static uint32_t s_last_pir_scan_time = 0;       // 上次红外扫描时间
static uint32_t s_last_display_update_time = 0; // 上次显示更新时间
static uint32_t s_last_wifi_upload_time = 0;    // 上次WiFi上传时间

/* 报警状态 */
static uint8_t s_alarm_active = 0;  // 报警状态标志:0-正常,1-报警中

/* 蜂鸣器控制宏 */
#define BUZZER_ON()   HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET)
#define BUZZER_OFF()  HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET)

/**
 * @brief  系统初始化
 * @note   初始化所有外设和传感器模块
 */
static void System_Init(void)
{
    /* 初始化OLED显示屏 */
    OLED_Init();
    OLED_Clear();
    OLED_ShowString(0, 0, "Smart Classroom", 16);
    OLED_ShowString(0, 20, "Initializing...", 12);
    OLED_UpdateScreen();
    HAL_Delay(1000);
    
    /* 初始化BH1750光照传感器 */
    BH1750_Init();
    
    /* 初始化人数统计模块 */
    PeopleCounter_Init();
    
    /* 初始化蜂鸣器为关闭状态 */
    BUZZER_OFF();
    
    /* 初始化ESP8266 WiFi模块(可选,如果不需要联网可注释掉) */
    OLED_Clear();
    OLED_ShowString(0, 0, "Connecting WiFi", 12);
    OLED_UpdateScreen();
    
    if(ESP8266_Init() == ESP8266_OK)
    {
        OLED_ShowString(0, 16, "ESP8266 OK", 12);
        OLED_UpdateScreen();
        
        if(ESP8266_ConnectWiFi() == ESP8266_OK)
        {
            OLED_ShowString(0, 32, "WiFi Connected", 12);
            OLED_UpdateScreen();
            HAL_Delay(500);
            
            /* 尝试连接MQTT服务器 */
            // ESP8266_MQTT_Connect();
        }
        else
        {
            OLED_ShowString(0, 32, "WiFi Failed!", 12);
            OLED_UpdateScreen();
        }
    }
    else
    {
        OLED_ShowString(0, 16, "ESP8266 Error", 12);
        OLED_UpdateScreen();
    }
    
    HAL_Delay(1500);
    
    /* MH-Z19B传感器预热(约3分钟,这里简化处理) */
    OLED_Clear();
    OLED_ShowString(0, 0, "Sensor Warmup", 12);
    OLED_ShowString(0, 16, "Please wait...", 12);
    OLED_UpdateScreen();
    HAL_Delay(3000);
}

/**
 * @brief  更新OLED显示内容
 * @note   显示格式:
 *         第一行:CO2: xxxx ppm
 *         第二行:Light: xxxx.x lux
 *         第三行:People: xx
 *         第四行:状态指示
 */
static void Update_Display(void)
{
    OLED_Clear();
    
    /* 第一行:显示CO₂浓度 */
    OLED_Printf(0, 0, 16, "CO2:%dppm", g_co2_concentration);
    
    /* 第二行:显示光照强度 */
    OLED_Printf(0, 20, 16, "Lux:%.1f", g_light_intensity);
    
    /* 第三行:显示人数 */
    OLED_Printf(0, 40, 16, "Ppl:%d", g_people_count);
    
    /* 第四行:显示报警状态 */
    if(s_alarm_active)
    {
        OLED_Printf(0, 56, 12, "ALARM! CO2 HIGH");
    }
    else
    {
        OLED_Printf(0, 56, 12, "Status: Normal");
    }
    
    /* 刷新到屏幕 */
    OLED_UpdateScreen();
}

/**
 * @brief  检查CO₂浓度并控制报警
 * @note   当CO₂浓度超过阈值时触发蜂鸣器报警
 *         报警状态会保持直到浓度回落到阈值以下
 */
static void Check_Alarm(void)
{
    if(g_co2_concentration > CO2_ALARM_THRESHOLD)
    {
        if(!s_alarm_active)
        {
            s_alarm_active = 1;
            BUZZER_ON();  // 开启蜂鸣器
        }
        else
        {
            /* 闪烁报警:蜂鸣器间歇响 */
            static uint32_t beep_timer = 0;
            if(HAL_GetTick() - beep_timer > 500)
            {
                beep_timer = HAL_GetTick();
                /* 切换蜂鸣器状态 */
                HAL_GPIO_TogglePin(BUZZER_GPIO_Port, BUZZER_Pin);
            }
        }
    }
    else
    {
        if(s_alarm_active)
        {
            s_alarm_active = 0;
            BUZZER_OFF();  // 关闭蜂鸣器
        }
    }
}

/**
 * @brief  通过WiFi上传传感器数据
 * @note   使用JSON格式组织数据,通过MQTT发布
 *         格式示例:{"co2":850,"light":320.5,"people":15}
 */
static void Upload_Data_To_Cloud(void)
{
    char json_payload[128];
    
    /* 构建JSON格式的数据包 */
    snprintf(json_payload, sizeof(json_payload),
             "{\"co2\":%d,\"light\":%.1f,\"people\":%d}",
             g_co2_concentration, g_light_intensity, g_people_count);
    
    /* 分别发布到不同的主题 */
    char co2_str[16], light_str[16], people_str[16];
    snprintf(co2_str, sizeof(co2_str), "%d", g_co2_concentration);
    snprintf(light_str, sizeof(light_str), "%.1f", g_light_intensity);
    snprintf(people_str, sizeof(people_str), "%d", g_people_count);
    
    ESP8266_MQTT_Publish(MQTT_TOPIC_CO2, co2_str);
    ESP8266_MQTT_Publish(MQTT_TOPIC_LIGHT, light_str);
    ESP8266_MQTT_Publish(MQTT_TOPIC_PEOPLE, people_str);
}

/**
 * @brief  主函数
 * @note   系统主循环,按照时间调度执行各任务
 */
int main(void)
{
    /* STM32 HAL库初始化 */
    HAL_Init();
    
    /* 系统时钟配置 */
    SystemClock_Config();
    
    /* 外设初始化(由CubeMX生成) */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_USART2_UART_Init();
    MX_I2C1_Init();
    MX_TIM2_Init();
    
    /* 系统模块初始化 */
    System_Init();
    
    /* 主循环 */
    while(1)
    {
        uint32_t current_time = HAL_GetTick();
        
        /* 任务1:CO₂浓度采集(每2秒) */
        if((current_time - s_last_co2_sample_time) >= CO2_SAMPLE_INTERVAL)
        {
            s_last_co2_sample_time = current_time;
            MHZ19B_ReadCO2();
        }
        
        /* 任务2:光照强度采集(每500ms) */
        if((current_time - s_last_light_sample_time) >= LIGHT_SAMPLE_INTERVAL)
        {
            s_last_light_sample_time = current_time;
            BH1750_GetLightIntensity();
        }
        
        /* 任务3:人体红外扫描(每100ms) */
        if((current_time - s_last_pir_scan_time) >= PIR_SCAN_INTERVAL)
        {
            s_last_pir_scan_time = current_time;
            PeopleCounter_Scan();
        }
        
        /* 任务4:OLED显示更新(每1秒) */
        if((current_time - s_last_display_update_time) >= DISPLAY_UPDATE_INTERVAL)
        {
            s_last_display_update_time = current_time;
            Update_Display();
        }
        
        /* 任务5:报警检查(每次循环都检查) */
        Check_Alarm();
        
        /* 任务6:WiFi数据上传(每10秒) */
        if((current_time - s_last_wifi_upload_time) >= WIFI_UPLOAD_INTERVAL)
        {
            s_last_wifi_upload_time = current_time;
            Upload_Data_To_Cloud();
        }
        
        /* 短暂延时,避免CPU占用过高 */
        HAL_Delay(10);
    }
}

/**
 * @brief  系统时钟配置
 * @note   HSE 8MHz → PLL ×9 → SYSCLK 72MHz
 *         AHB = 72MHz, APB1 = 36MHz, APB2 = 72MHz
 */
void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    
    /* 配置HSE振荡器 */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    HAL_RCC_OscConfig(&RCC_OscInitStruct);
    
    /* 配置系统时钟 */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}

系统主程序运行流程如下:














系统上电启动
HAL库初始化
系统时钟配置

72MHz
外设GPIO初始化
USART1/USART2初始化
I2C1初始化
定时器初始化
系统模块初始化
OLED显示屏初始化
BH1750光照传感器初始化
人数统计模块初始化
ESP8266 WiFi初始化
WiFi连接

成功?
MQTT服务器连接
传感器预热等待
进入主循环
检查各任务

时间间隔
CO₂采样

2秒到?
读取CO₂浓度
光照采样

500ms到?
读取光照强度
红外扫描

100ms到?
人体红外扫描

人数统计
显示更新

1秒到?
更新OLED显示
CO₂浓度

超阈值?
蜂鸣器报警
关闭蜂鸣器
WiFi上传

10秒到?
JSON数据上传云端
延时10ms

编译、下载与调试

KEIL编译设置

  1. 在Keil中打开工程,点击"Options for Target"(魔术棒图标)
  2. 在"Target"选项卡中确认芯片型号为STM32F103C8
  3. 在"Output"选项卡中勾选"Create HEX File"
  4. 在"C/C++"选项卡中,将优化级别设置为"-O1"或"-O0"(调试阶段建议不优化)
  5. 在"Debug"选项卡中选择调试器类型(ST-Link/J-Link等)
  6. 点击"Build"(F7)编译工程,确保无错误

程序下载

  1. 使用ST-Link/V2下载器连接STM32最小系统板

    • SWCLK → SWCLK
    • SWDIO → SWDIO
    • GND → GND
    • 3.3V → 3.3V
  2. 在Keil中点击"Download"(F8)将程序下载到芯片

  3. 也可以使用串口下载(需要设置BOOT0=1, BOOT1=0)

    • 使用FlyMcu等ISP下载工具
    • 选择生成的HEX文件进行下载

调试技巧

  1. 串口调试:可以在代码中添加printf输出调试信息,通过USART1输出到串口助手
  2. 断点调试:使用ST-Link在Keil中设置断点,单步执行观察变量值
  3. LED指示:可以在关键位置加入LED闪烁,直观判断程序运行状态
  4. 传感器测试:先用单独的测试代码验证每个传感器是否正常工作

实际部署与测试

硬件安装注意事项

  1. CO₂传感器安装

    • MH-Z19B需要安装在通风良好但避免直接风吹的位置
    • 传感器需要3-5分钟预热才能获得稳定读数
    • 建议安装在教室中央,高度约1.5米处
  2. 人体红外传感器安装

    • 两个HC-SR501分别安装在门框内外两侧
    • 传感器探测范围需要覆盖门口区域
    • 调整延时旋钮至最小值,封锁时间调至最小
    • 两个传感器之间保持一定距离(至少30cm),避免同时触发
  3. 光照传感器安装

    • BH1750需要避免阳光直射
    • 安装在课桌高度位置,模拟学生实际感受的光照
  4. 供电要求

    • 系统总功耗约200-300mA
    • 建议使用5V 1A以上的电源适配器供电
    • ESP8266峰值电流可达300mA,需保证供电充足

功能测试清单

测试项目 测试方法 预期结果 通过标准
CO₂采集 对着传感器呼吸 CO₂数值上升 数值有明显变化
光照采集 用手遮挡传感器 光照值下降 数值响应灵敏
人数统计-进入 从门外走进教室 人数+1 计数准确
人数统计-离开 从教室内走出 人数-1 计数准确
OLED显示 观察屏幕 显示所有参数 信息完整清晰
CO₂报警 在传感器附近呼气 蜂鸣器响起 超过1500ppm报警
WiFi上传 查看云平台 接收数据 数据实时更新

常见问题排查

  1. OLED不显示

    • 检查I2C接线是否正确(SCL→PB6, SDA→PB7)
    • 检查设备地址是否正确(0x3C)
    • 使用逻辑分析仪或示波器检查I2C信号
  2. CO₂读数异常

    • 确保传感器已预热3分钟以上
    • 检查串口接线(TX→PA10, RX→PA9)
    • 检查波特率是否为9600
  3. 人数统计不准确

    • 调整HC-SR501的灵敏度和延时旋钮
    • 确认两个传感器的安装位置和方向
    • 检查GPIO引脚配置是否正确
  4. ESP8266连接失败

    • 确认WiFi名称和密码正确
    • 检查供电是否充足(独立3.3V供电)
    • 使用串口助手直接发送AT指令测试模块

总结与展望

本项目基于STM32F103C8T6成功实现了智慧教室环境监控系统,具备CO₂浓度监测、光照检测、人员统计三大核心功能。系统采用模块化设计,各传感器驱动独立封装,便于维护和扩展。

已实现功能

  • ✅ 实时CO₂浓度采集与显示
  • ✅ 光照强度检测
  • ✅ 教室人数自动统计
  • ✅ 本地OLED屏幕显示
  • ✅ CO₂超标蜂鸣器报警
  • ✅ WiFi数据上传至云平台

可扩展方向

  • 添加温湿度传感器(如DHT22),实现更全面的环境监测
  • 增加SD卡模块,实现数据本地存储和历史记录查询
  • 添加继电器模块,实现自动控制新风系统或空调
  • 开发手机APP或Web端可视化面板
  • 使用FreeRTOS操作系统优化多任务调度
  • 增加语音播报功能,在CO₂超标时语音提醒开窗通风

通过本教程,读者应该能够掌握STM32多传感器协同工作的开发方法,以及从硬件搭建、驱动编写到系统集成的完整流程。希望这个实战项目能帮助你迈入嵌入式系统开发的大门。

相关推荐
yqcoder1 小时前
Vue 的心脏:深度解析 Vue 2 vs Vue 3 响应式机制
前端·javascript·vue.js
振南的单片机世界2 小时前
推挽输出:上管推、下管拉,驱动强但不“合群”
arm开发·stm32·单片机·嵌入式硬件
东方小月2 小时前
Claude Code Skill 完全指南:一个 markdown 文件,就是一个专家分身
前端·后端
DianSan_ERP2 小时前
抖店订单接口中消费者信息加密解密机制与安全履约全解析
前端·网络·数据库·后端·安全·团队开发·运维开发
PBitW2 小时前
一个skill,让项目管理和写绩效变得简单!
前端·trae
Dxy12393102162 小时前
CSS中的filter属性详解
前端·css
森利威尔电子-2 小时前
森利威尔SL7140|2.5–24V 宽压 / 10mA–2A / PWM 调光 线性 LED 恒流驱动
单片机·嵌入式硬件·集成电路·芯片·电源芯片
Vincent_czr3 小时前
iOS中常常遇到后端返回JSON出现null值问题
前端
问心无愧05133 小时前
ctf show web入门90
前端·笔记