文章目录
项目背景与系统概述
随着智慧校园建设的推进,教室环境质量对学生的学习效率和身体健康有着直接影响。二氧化碳浓度过高会导致学生注意力不集中、昏昏欲睡;光照强度不足或过强则会影响视力和学习舒适度;同时,统计教室人员数量可以为后勤管理、空调照明节能控制提供数据支撑。
本项目基于STM32F103C8T6微控制器,设计一套完整的智慧教室环境监控系统,实现对CO₂浓度、光照强度、教室人数的实时采集与显示,并配备数据异常报警功能。系统整体架构如下:
STM32F103C8T6
主控芯片
MH-Z19B
CO₂传感器
BH1750
光照传感器
HC-SR501×2
人体红外传感器
0.96寸OLED
显示模块
有源蜂鸣器
报警模块
ESP8266-01S
WiFi模块
阿里云IoT平台
手机APP/Web端
系统功能需求分析
本系统需要实现以下核心功能:
- CO₂浓度监测:通过MH-Z19B红外二氧化碳传感器实时采集教室内CO₂浓度,精度可达±50ppm
- 光照强度检测:利用BH1750数字光照传感器测量环境光照度,范围1-65535 lux
- 人数统计:采用两个HC-SR501人体红外传感器配合逻辑判断,实现人员进出检测
- 本地数据显示:通过0.96寸OLED屏幕实时显示各项参数
- 阈值报警:CO₂浓度超过设定阈值时,蜂鸣器发出声光报警
- 数据上云:通过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
软件开发环境搭建
开发工具安装
-
Keil MDK-ARM V5:STM32官方推荐的集成开发环境
- 下载地址:https://www.keil.com/download/product/
- 安装后需要安装STM32F1系列的器件包
-
STM32CubeMX:图形化配置工具,用于生成初始化代码
- 下载地址:https://www.st.com/en/development-tools/stm32cubemx.html
- 需要安装Java运行环境(JRE)
-
串口调试助手:用于调试传感器和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工程创建与配置
- 打开Keil MDK-ARM V5
- 选择"Project" → "Open Project",找到STM32CubeMX生成的工程文件
- 如果提示安装器件包,根据提示安装对应的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编译设置
- 在Keil中打开工程,点击"Options for Target"(魔术棒图标)
- 在"Target"选项卡中确认芯片型号为STM32F103C8
- 在"Output"选项卡中勾选"Create HEX File"
- 在"C/C++"选项卡中,将优化级别设置为"-O1"或"-O0"(调试阶段建议不优化)
- 在"Debug"选项卡中选择调试器类型(ST-Link/J-Link等)
- 点击"Build"(F7)编译工程,确保无错误
程序下载
-
使用ST-Link/V2下载器连接STM32最小系统板
- SWCLK → SWCLK
- SWDIO → SWDIO
- GND → GND
- 3.3V → 3.3V
-
在Keil中点击"Download"(F8)将程序下载到芯片
-
也可以使用串口下载(需要设置BOOT0=1, BOOT1=0)
- 使用FlyMcu等ISP下载工具
- 选择生成的HEX文件进行下载
调试技巧
- 串口调试:可以在代码中添加printf输出调试信息,通过USART1输出到串口助手
- 断点调试:使用ST-Link在Keil中设置断点,单步执行观察变量值
- LED指示:可以在关键位置加入LED闪烁,直观判断程序运行状态
- 传感器测试:先用单独的测试代码验证每个传感器是否正常工作
实际部署与测试
硬件安装注意事项
-
CO₂传感器安装:
- MH-Z19B需要安装在通风良好但避免直接风吹的位置
- 传感器需要3-5分钟预热才能获得稳定读数
- 建议安装在教室中央,高度约1.5米处
-
人体红外传感器安装:
- 两个HC-SR501分别安装在门框内外两侧
- 传感器探测范围需要覆盖门口区域
- 调整延时旋钮至最小值,封锁时间调至最小
- 两个传感器之间保持一定距离(至少30cm),避免同时触发
-
光照传感器安装:
- BH1750需要避免阳光直射
- 安装在课桌高度位置,模拟学生实际感受的光照
-
供电要求:
- 系统总功耗约200-300mA
- 建议使用5V 1A以上的电源适配器供电
- ESP8266峰值电流可达300mA,需保证供电充足
功能测试清单
| 测试项目 | 测试方法 | 预期结果 | 通过标准 |
|---|---|---|---|
| CO₂采集 | 对着传感器呼吸 | CO₂数值上升 | 数值有明显变化 |
| 光照采集 | 用手遮挡传感器 | 光照值下降 | 数值响应灵敏 |
| 人数统计-进入 | 从门外走进教室 | 人数+1 | 计数准确 |
| 人数统计-离开 | 从教室内走出 | 人数-1 | 计数准确 |
| OLED显示 | 观察屏幕 | 显示所有参数 | 信息完整清晰 |
| CO₂报警 | 在传感器附近呼气 | 蜂鸣器响起 | 超过1500ppm报警 |
| WiFi上传 | 查看云平台 | 接收数据 | 数据实时更新 |
常见问题排查
-
OLED不显示:
- 检查I2C接线是否正确(SCL→PB6, SDA→PB7)
- 检查设备地址是否正确(0x3C)
- 使用逻辑分析仪或示波器检查I2C信号
-
CO₂读数异常:
- 确保传感器已预热3分钟以上
- 检查串口接线(TX→PA10, RX→PA9)
- 检查波特率是否为9600
-
人数统计不准确:
- 调整HC-SR501的灵敏度和延时旋钮
- 确认两个传感器的安装位置和方向
- 检查GPIO引脚配置是否正确
-
ESP8266连接失败:
- 确认WiFi名称和密码正确
- 检查供电是否充足(独立3.3V供电)
- 使用串口助手直接发送AT指令测试模块
总结与展望
本项目基于STM32F103C8T6成功实现了智慧教室环境监控系统,具备CO₂浓度监测、光照检测、人员统计三大核心功能。系统采用模块化设计,各传感器驱动独立封装,便于维护和扩展。
已实现功能:
- ✅ 实时CO₂浓度采集与显示
- ✅ 光照强度检测
- ✅ 教室人数自动统计
- ✅ 本地OLED屏幕显示
- ✅ CO₂超标蜂鸣器报警
- ✅ WiFi数据上传至云平台
可扩展方向:
- 添加温湿度传感器(如DHT22),实现更全面的环境监测
- 增加SD卡模块,实现数据本地存储和历史记录查询
- 添加继电器模块,实现自动控制新风系统或空调
- 开发手机APP或Web端可视化面板
- 使用FreeRTOS操作系统优化多任务调度
- 增加语音播报功能,在CO₂超标时语音提醒开窗通风
通过本教程,读者应该能够掌握STM32多传感器协同工作的开发方法,以及从硬件搭建、驱动编写到系统集成的完整流程。希望这个实战项目能帮助你迈入嵌入式系统开发的大门。