前言
本篇文章基于 STM32F103C8T6 单片机,实现 Y01‑3IN1 三合一空气质量模块数据读取,通过串口接收解析 TVOC、HCHO 甲醛、CO₂参考浓度,最终在 OLED 屏幕上实时展示测量数据。
Y01‑3IN1 模块内部完成气体信号采集与换算,采用 UART 串口每秒主动上报数据,和传统 MQ‑2 这类模拟输出传感器实现方式有很大区别,非常适合单片机串口通信、数据帧解析、中断接收的教学实验。
注意:Y01‑3IN1 的 HCHO、CO₂为算法推算参考值,并非高精度专用传感器,仅用于教学演示、观察空气质量变化趋势,不能用于精密检测、安全防护。
一、项目实现功能流程
整体工作流程:
- 环境中挥发性气体发生改变,Y01‑3IN1 内部气敏元件产生响应
- 模块内部电路采集气敏信号,算法换算出 TVOC、HCHO、CO₂数据
- 模块通过 UART 串口,每秒主动发送一帧 9 字节数据
- STM32 的 USART3 使用中断方式接收字节,查找帧头、校验数据帧
- 解析成功之后把浓度数值送到 OLED 屏幕进行显示
简单概括:Y01‑3in1 将气体变化转为串口数字帧,STM32 完成接收、校验、解析,最后 OLED 展示结果。

和 MQ‑2 模拟传感器对比
| 对比项 | MQ‑2 演示 | Y01‑3in1 演示 |
|---|---|---|
| 模块输出 | AO 模拟电压 | UART 数字数据帧 |
| STM32 外设 | ADC1 | USART3 |
| 数据处理 | ADC 采样、滤波、电压换算 | 帧头同步、校验和验证、数据字段解析 |
| 输出内容 | ADC 采样值、电压电平 | TVOC、CO₂、HCHO 浓度参考值 |
二、硬件接线说明
使用 STM32F103C8T6 最小系统板,Y01‑3IN1 空气质量模块,I2C 接口 OLED 屏幕。
接线重点
串口交叉接线:模块 TX 接单片机 RX,模块 RX 接单片机 TX
- Y01 A(RX) ←── PB10(USART3_TX)
- Y01 B(TX) ──→ PB11(USART3_RX)
本项目使用模块默认主动上报模式,单片机主要工作在接收数据,PB10 发送引脚硬件完整配置,但程序中不需要发送指令。
供电与电平提醒
- Y01‑3IN1 模块需要5V 供电,串口输出为 5V 电平;STM32F103 的 PB11 引脚可以容忍 5V 输入。如果换成其他 MCU,一定要确认 IO 耐压,必要时增加电平转换或者限流电阻。
- 所有器件必须共地! 不共地会出现收不到数据、校验全部报错的问题。
三、Y01‑3IN1 传感器检测原理
模块内部是 TVOC 广谱气敏元件,当空气中酒精、香水、油烟等挥发性气体接触传感器,元件电阻 / 电导会发生改变。模块内部 MCU 采集这个变化,运行算法计算出 TVOC、HCHO、CO₂三组参考数值,通过串口向外发送。
⚠️重要知识点:
- TVOC 代表总挥发性有机物,是多种气体的综合响应,不能识别单一气体。
- 本模块 CO₂、HCHO 属于软件推算参考值,不是红外二氧化碳传感器,也不是电化学甲醛传感器。
- 酒精、香水、清洁剂等挥发物,会同时干扰三个测量读数。
- 定位:教学演示、观察空气质量变化趋势,不可做精密计量、安全报警设备。
四、串口通信协议详解
4.1 串口基础参数
使用 STM32 的 USART3,参数 9600 8N1
| 参数 | 设置 |
|---|---|
| 串口外设 | USART3 |
| TX 引脚 | PB10 |
| RX 引脚 | PB11 |
| 波特率 | 9600 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | 无 |
| 硬件流控 | 关闭 |
4.2 主动上传数据帧格式
模块默认每秒自动发送 1 帧,每帧固定 9 字节
| 字节序号 | 名称 | 说明 |
|---|---|---|
| B1 | 帧头 1 | 固定 0x2C |
| B2 | 帧头 2 | 固定 0xE4 |
| B3 | TVOC_H | TVOC 高字节 |
| B4 | TVOC_L | TVOC 低字节 |
| B5 | HCHO_H | HCHO 高字节 |
| B6 | HCHO_L | HCHO 低字节 |
| B7 | CO2_H | CO₂高字节 |
| B8 | CO2_L | CO₂低字节 |
| B9 | SUM 校验和 | 前 8 字节累加,取低 8 位 |
帧格式简写:2C E4 TVOC_H TVOC_L HCHO_H HCHO_L CO2_H CO2_L SUM
4.3 校验和计算规则
cpp
SUM = (B1 + B2 + B3 + B4 + B5 + B6 + B7 + B8) & 0xFF
举例接收帧:2C E4 00 0A 00 03 01 68 86 把前 8 个字节全部相加得到 0x186,取低 8 位 0x86,和最后一个字节相等,代表本帧数据有效。 校验失败的帧直接丢弃,不会更新屏幕数据,程序会统计校验错误计数。
4.4 数据换算公式
高低字节合并为 16 位原始数据: raw = (高字节 << 8) | 低字节
单位换算:
- TVOC (mg/m³) = TVOC 原始值 × 0.001
- HCHO (mg/m³) = HCHO 原始值 × 0.001
- CO₂(ppm) = CO₂原始值
工程技巧:STM32F103(Cortex‑M3)浮点性能弱,项目中可以把 TVOC、HCHO 放大 1000 倍用整型存储,减少浮点数运算,避免显示误差。
示例解析: TVOC 原始 = 10 → 10 ×0.001 =0.010 mg/m³ HCHO 原始 = 3 →3 ×0.001 =0.003 mg/m³ CO₂原始 = 360 →360 ppm
五、程序设计思路
5.1 为什么用串口中断接收
模块随时上报数据,主循环在刷新 OLED 屏幕,如果程序阻塞等待串口,会造成界面卡顿;如果不及时读取串口寄存器,字节直接丢失。
使用 USART3 接收中断:每收到 1 个字节立刻进入中断服务函数,把字节交给帧解析状态机处理,处理完立刻返回主循环,不阻塞 OLED 刷新等业务。
5.2 帧同步状态机逻辑
串口是连续字节流,硬件不会自动按 9 字节分组,软件必须手动寻找帧头0x2C 0xE4:
- 空闲状态循环查找第一个帧头字节
0x2C - 收到
0x2C之后,判断下一字节是否等于0xE4 - 帧头匹配成功,继续接收剩下 7 个字节,凑齐完整 9 字节
- 执行校验和判断,校验成功保存数据;校验失败统计错误
- 无论帧解析成功或者失败,重新回到寻找帧头状态
就算上电的时候,单片机正好收到半帧数据,经过状态机同步,后续收到正确帧头就可以恢复正常解析,抗干扰能力更强。
5.3 中断与主循环数据保护
中断随时会改写传感器数据,主循环读取的时候如果正好触发中断,有可能读到半旧半新的混合错误数据。 解决方法:读取数据快照的时候短暂关闭串口中断,复制完成马上打开中断,获取一帧完整、一致性的数据。中断关闭时间极短,不会影响每秒一次的数据接收。
5.4 两种工作模式说明
- 主动上传模式(本项目使用) 上电默认开启,模块每秒主动输出一帧数据,单片机不需要发送任何指令,程序逻辑简单,适合教学。
- 问答模式 单片机发送命令
FF 01 86,模块切换问答模式,停止自动上报,需要单片机主动查询才返回数据。一旦进入问答模式,只有重新断电上电才能恢复主动上传。
坑点:如果代码误调用切换问答模式,之后不循环发送查询指令,OLED 屏幕会停止刷新。
六、源代码相关
这里包含驱动文件、main 主函数代码
主要工程文件清单:
Y01_3IN1.h头文件,结构体、函数声明Y01_3IN1.c驱动实现:初始化、中断回调、字节处理、帧校验解析、读取数据快照main.c主函数:硬件初始化、循环读取传感器数据,调用 OLED 显示函数
核心函数简介:
Y01_3IN1_Init():初始化串口驱动USART3_IRQHandler():串口 3 中断服务函数Y01_3IN1_HandleReceivedByte():单字节处理,状态机帧同步Y01_3IN1_Read():获取传感器数据快照,供 main 函数调用
maic.c代码
cpp
/* Y01-3in1 air-quality teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "Y01_3IN1.h"
#define DISPLAY_POLL_TIME_MS 50U
static void Y01_3IN1_ShowWaiting(void);
static void Y01_3IN1_ShowData(const Y01_3IN1_Data *data);
static u8 Y01_3IN1_DigitCount(u16 value);
int main(void)
{
Y01_3IN1_Data air_data;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
Y01_3IN1_Init();
Y01_3IN1_ShowWaiting();
while(1)
{
/* The module uploads a frame automatically about once per second. */
if(Y01_3IN1_Read(&air_data) != 0U)
{
Y01_3IN1_ShowData(&air_data);
}
delay_ms(DISPLAY_POLL_TIME_MS);
}
}
static void Y01_3IN1_ShowWaiting(void)
{
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"Y01 AIR QUALITY", 12);
OLED_ShowString(0, 20, (u8 *)"WARMING UP...", 12);
OLED_ShowString(0, 36, (u8 *)"WAITING FOR DATA", 12);
OLED_ShowString(0, 52, (u8 *)"USART3 PB10/PB11", 12);
OLED_Refresh();
}
static void Y01_3IN1_ShowData(const Y01_3IN1_Data *data)
{
u16 whole;
u16 fraction;
u8 digits;
u8 x;
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"Y01 AIR QUALITY", 12);
/* TVOC is displayed as x.xxx mg/m3. */
OLED_ShowString(0, 16, (u8 *)"TVOC:", 12);
whole = data->tvoc_mg_m3_x1000 / 1000U;
fraction = data->tvoc_mg_m3_x1000 % 1000U;
digits = Y01_3IN1_DigitCount(whole);
OLED_ShowNum(30, 16, whole, digits, 12);
x = (u8)(30U + digits * 6U);
OLED_ShowChar(x, 16, '.', 12);
OLED_ShowNum((u8)(x + 6U), 16, fraction, 3, 12);
OLED_ShowString((u8)(x + 24U), 16, (u8 *)"mg/m3", 12);
/* Keep the order requested for the teaching screen: TVOC, CO2, HCHO. */
OLED_ShowString(0, 32, (u8 *)"CO2 :", 12);
digits = Y01_3IN1_DigitCount(data->co2_ppm);
OLED_ShowNum(30, 32, data->co2_ppm, digits, 12);
OLED_ShowString((u8)(30U + digits * 6U), 32, (u8 *)"ppm", 12);
OLED_ShowString(0, 48, (u8 *)"HCHO:", 12);
whole = data->hcho_mg_m3_x1000 / 1000U;
fraction = data->hcho_mg_m3_x1000 % 1000U;
digits = Y01_3IN1_DigitCount(whole);
OLED_ShowNum(30, 48, whole, digits, 12);
x = (u8)(30U + digits * 6U);
OLED_ShowChar(x, 48, '.', 12);
OLED_ShowNum((u8)(x + 6U), 48, fraction, 3, 12);
OLED_ShowString((u8)(x + 24U), 48, (u8 *)"mg/m3", 12);
OLED_Refresh();
}
static u8 Y01_3IN1_DigitCount(u16 value)
{
if(value >= 10000U)
{
return 5U;
}
if(value >= 1000U)
{
return 4U;
}
if(value >= 100U)
{
return 3U;
}
if(value >= 10U)
{
return 2U;
}
return 1U;
}
Y01_3IN1.c代码
cpp
#include "Y01_3IN1.h"
#include "misc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
/* Y01-3in1 使用 USART3 的默认引脚映射:PB10 发送、PB11 接收。 */
#define Y01_3IN1_USART USART3
#define Y01_3IN1_USART_CLK RCC_APB1Periph_USART3
#define Y01_3IN1_GPIO_PORT GPIOB
#define Y01_3IN1_GPIO_CLK RCC_APB2Periph_GPIOB
#define Y01_3IN1_TX_PIN GPIO_Pin_10
#define Y01_3IN1_RX_PIN GPIO_Pin_11
/* 串口中断逐字节填充此缓冲区,凑齐 9 字节后进行校验和解析。 */
static volatile u8 y01_rx_frame[Y01_3IN1_FRAME_SIZE];
static volatile u8 y01_rx_index;
static volatile u8 y01_new_frame;
/* 保存最近一帧有效数据及接收统计信息。 */
static volatile u16 y01_tvoc_x1000;
static volatile u16 y01_hcho_x1000;
static volatile u16 y01_co2_ppm;
static volatile u32 y01_rx_bytes;
static volatile u32 y01_valid_frames;
static volatile u32 y01_checksum_errors;
static u8 Y01_3IN1_Checksum(const volatile u8 *frame);
static void Y01_3IN1_SendByte(u8 byte);
/* 使用模块默认的 9600 波特率初始化驱动。 */
void Y01_3IN1_Init(void)
{
Y01_3IN1_InitWithBaudRate(Y01_3IN1_DEFAULT_BAUDRATE);
}
/* 初始化 GPIO、USART3 和接收中断。 */
void Y01_3IN1_InitWithBaudRate(u32 baudrate)
{
GPIO_InitTypeDef gpio;
USART_InitTypeDef usart;
NVIC_InitTypeDef nvic;
if(baudrate == 0U)
{
baudrate = Y01_3IN1_DEFAULT_BAUDRATE;
}
/* 分别开启 GPIOB、复用功能模块和 USART3 的外设时钟。 */
RCC_APB2PeriphClockCmd(Y01_3IN1_GPIO_CLK | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(Y01_3IN1_USART_CLK, ENABLE);
/* 关闭 USART3 重映射,使用默认的 TX/PB10、RX/PB11。 */
GPIO_PinRemapConfig(GPIO_FullRemap_USART3, DISABLE);
/* PB10 配置为复用推挽输出,用于向模块发送问答模式命令。 */
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = Y01_3IN1_TX_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(Y01_3IN1_GPIO_PORT, &gpio);
/* PB11 配置为上拉输入,用于接收模块每秒主动上传的数据。 */
gpio.GPIO_Pin = Y01_3IN1_RX_PIN;
gpio.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(Y01_3IN1_GPIO_PORT, &gpio);
/* 串口工作方式为 8N1,同时启用发送和接收功能。 */
USART_DeInit(Y01_3IN1_USART);
USART_StructInit(&usart);
usart.USART_BaudRate = baudrate;
usart.USART_WordLength = USART_WordLength_8b;
usart.USART_StopBits = USART_StopBits_1;
usart.USART_Parity = USART_Parity_No;
usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(Y01_3IN1_USART, &usart);
/* 开启 USART3 中断通道,串口每收到一个字节便进入中断处理。 */
nvic.NVIC_IRQChannel = USART3_IRQn;
nvic.NVIC_IRQChannelPreemptionPriority = 2U;
nvic.NVIC_IRQChannelSubPriority = 2U;
nvic.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic);
Y01_3IN1_Reset();
USART_ITConfig(Y01_3IN1_USART, USART_IT_RXNE, ENABLE);
USART_Cmd(Y01_3IN1_USART, ENABLE);
}
/* 清空解析器、最近测量值及所有接收统计信息。 */
void Y01_3IN1_Reset(void)
{
u8 i;
/* 清空共享变量时暂时关闭接收中断,避免中断同时修改这些变量。 */
USART_ITConfig(Y01_3IN1_USART, USART_IT_RXNE, DISABLE);
for(i = 0U; i < Y01_3IN1_FRAME_SIZE; i++)
{
y01_rx_frame[i] = 0U;
}
y01_rx_index = 0U;
y01_new_frame = 0U;
y01_tvoc_x1000 = 0U;
y01_hcho_x1000 = 0U;
y01_co2_ppm = 0U;
y01_rx_bytes = 0U;
y01_valid_frames = 0U;
y01_checksum_errors = 0U;
USART_ITConfig(Y01_3IN1_USART, USART_IT_RXNE, ENABLE);
}
/* 以原子快照的方式读取最近一帧有效数据。 */
u8 Y01_3IN1_Read(Y01_3IN1_Data *data)
{
u8 is_new;
if(data == 0)
{
return 0U;
}
/* 复制多个字段时关闭接收中断,防止得到前后两帧混合的数据。 */
USART_ITConfig(Y01_3IN1_USART, USART_IT_RXNE, DISABLE);
data->tvoc_mg_m3_x1000 = y01_tvoc_x1000;
data->hcho_mg_m3_x1000 = y01_hcho_x1000;
data->co2_ppm = y01_co2_ppm;
data->rx_bytes = y01_rx_bytes;
data->valid_frames = y01_valid_frames;
data->checksum_errors = y01_checksum_errors;
is_new = y01_new_frame;
y01_new_frame = 0U;
USART_ITConfig(Y01_3IN1_USART, USART_IT_RXNE, ENABLE);
return is_new;
}
/* 校验帧头和累加校验和,然后将高低字节组合为 16 位数据。 */
u8 Y01_3IN1_ParseFrame(const u8 frame[Y01_3IN1_FRAME_SIZE],
Y01_3IN1_Data *data)
{
if((frame == 0) || (data == 0))
{
return Y01_3IN1_ERROR_ARGUMENT;
}
/* 前两个字节必须是模块固定地址 0x2C、0xE4。 */
if((frame[0] != Y01_3IN1_FRAME_HEADER_1) ||
(frame[1] != Y01_3IN1_FRAME_HEADER_2))
{
return Y01_3IN1_ERROR_HEADER;
}
/* 第 9 字节必须等于前 8 字节累加结果的低 8 位。 */
if(Y01_3IN1_Checksum(frame) != frame[8])
{
return Y01_3IN1_ERROR_CHECKSUM;
}
data->tvoc_mg_m3_x1000 = (u16)(((u16)frame[2] << 8) | frame[3]);
data->hcho_mg_m3_x1000 = (u16)(((u16)frame[4] << 8) | frame[5]);
data->co2_ppm = (u16)(((u16)frame[6] << 8) | frame[7]);
return Y01_3IN1_OK;
}
/*
* 流式数据帧解析状态机:
* 先寻找连续的帧头 0x2C、0xE4,再接收剩余 7 个字节。
*/
void Y01_3IN1_HandleReceivedByte(u8 byte)
{
y01_rx_bytes++;
/* 等待第一个帧头字节 0x2C,其他无关字节直接丢弃。 */
if(y01_rx_index == 0U)
{
if(byte == Y01_3IN1_FRAME_HEADER_1)
{
y01_rx_frame[0] = byte;
y01_rx_index = 1U;
}
return;
}
/* 检查第二个帧头字节 0xE4,并在通信干扰后自动重新同步。 */
if(y01_rx_index == 1U)
{
if(byte == Y01_3IN1_FRAME_HEADER_2)
{
y01_rx_frame[1] = byte;
y01_rx_index = 2U;
}
else if(byte == Y01_3IN1_FRAME_HEADER_1)
{
y01_rx_frame[0] = byte;
}
else
{
y01_rx_index = 0U;
}
return;
}
/* 帧头正确后,按顺序保存剩余数据和校验和。 */
y01_rx_frame[y01_rx_index] = byte;
y01_rx_index++;
if(y01_rx_index < Y01_3IN1_FRAME_SIZE)
{
return;
}
/* 只有校验正确的数据才会更新到主循环可读取的测量结果中。 */
if(Y01_3IN1_Checksum(y01_rx_frame) == y01_rx_frame[8])
{
y01_tvoc_x1000 = (u16)(((u16)y01_rx_frame[2] << 8) |
y01_rx_frame[3]);
y01_hcho_x1000 = (u16)(((u16)y01_rx_frame[4] << 8) |
y01_rx_frame[5]);
y01_co2_ppm = (u16)(((u16)y01_rx_frame[6] << 8) |
y01_rx_frame[7]);
y01_valid_frames++;
y01_new_frame = 1U;
}
else
{
y01_checksum_errors++;
}
y01_rx_index = 0U;
}
/* 发送问答模式命令 FF 01 86;正常主动上传时无需调用。 */
void Y01_3IN1_RequestMeasurement(void)
{
Y01_3IN1_SendByte(0xFFU);
Y01_3IN1_SendByte(0x01U);
Y01_3IN1_SendByte(0x86U);
}
/* 计算前 8 个字节累加和的低 8 位。 */
static u8 Y01_3IN1_Checksum(const volatile u8 *frame)
{
u8 checksum = 0U;
u8 i;
for(i = 0U; i < 8U; i++)
{
checksum = (u8)(checksum + frame[i]);
}
return checksum;
}
/* 阻塞等待发送数据寄存器空闲,然后发送一个字节。 */
static void Y01_3IN1_SendByte(u8 byte)
{
while(USART_GetFlagStatus(Y01_3IN1_USART, USART_FLAG_TXE) == RESET)
{
}
USART_SendData(Y01_3IN1_USART, byte);
}
/* USART3 接收中断服务函数。 */
void USART3_IRQHandler(void)
{
if(USART_GetITStatus(Y01_3IN1_USART, USART_IT_RXNE) != RESET)
{
Y01_3IN1_HandleReceivedByte(
(u8)(USART_ReceiveData(Y01_3IN1_USART) & 0x00FFU));
}
else if((USART_GetFlagStatus(Y01_3IN1_USART, USART_FLAG_ORE) != RESET) ||
(USART_GetFlagStatus(Y01_3IN1_USART, USART_FLAG_NE) != RESET) ||
(USART_GetFlagStatus(Y01_3IN1_USART, USART_FLAG_FE) != RESET))
{
/* 先读取状态寄存器再读取数据寄存器,可以清除串口接收错误标志。 */
(void)USART_ReceiveData(Y01_3IN1_USART);
}
}
Y01_3IN1.h代码
cpp
#ifndef __Y01_3IN1_H
#define __Y01_3IN1_H
#include "stm32f10x.h"
/*
* Y01-3in1 模块接线:
* A(模块接收端)-> PB10(USART3_TX)
* B(模块发送端)-> PB11(USART3_RX)
*
* 串口参数:9600 波特率、8 位数据位、无校验、1 位停止位。
* 模块默认每秒主动上传一帧数据,帧格式如下:
* 2C E4 TVOC高字节 TVOC低字节 HCHO高字节 HCHO低字节
* CO2高字节 CO2低字节 累加校验和
*/
#define Y01_3IN1_FRAME_SIZE 9U
#define Y01_3IN1_FRAME_HEADER_1 0x2CU
#define Y01_3IN1_FRAME_HEADER_2 0xE4U
#define Y01_3IN1_DEFAULT_BAUDRATE 9600U
#define Y01_3IN1_OK 0U
#define Y01_3IN1_ERROR_ARGUMENT 1U
#define Y01_3IN1_ERROR_HEADER 2U
#define Y01_3IN1_ERROR_CHECKSUM 3U
typedef struct
{
/* TVOC 和 HCHO 的一个原始计数代表 0.001 mg/m3。 */
u16 tvoc_mg_m3_x1000;
u16 hcho_mg_m3_x1000;
/* CO2 数据的单位为 ppm,不需要进行小数换算。 */
u16 co2_ppm;
/* 接收统计信息,可用于教学演示和串口通信故障排查。 */
u32 rx_bytes;
u32 valid_frames;
u32 checksum_errors;
} Y01_3IN1_Data;
/* 配置 PB10、PB11 和 USART3,并清空数据帧解析状态。 */
void Y01_3IN1_Init(void);
void Y01_3IN1_InitWithBaudRate(u32 baudrate);
/* 清空最近一次测量值、接收统计信息和数据帧解析状态。 */
void Y01_3IN1_Reset(void);
/*
* 将最近一次有效测量数据复制到 data。
* 每收到一帧新的有效数据,只返回一次 1;没有新数据时返回 0。
* 即使返回 0,data 中仍会保存最近一次接收到的测量值和统计信息。
*/
u8 Y01_3IN1_Read(Y01_3IN1_Data *data);
/* 校验并解析一帧完整的 9 字节数据,便于协议教学和独立测试。 */
u8 Y01_3IN1_ParseFrame(const u8 frame[Y01_3IN1_FRAME_SIZE],
Y01_3IN1_Data *data);
/* 向流式数据帧解析器送入一个串口接收字节。 */
void Y01_3IN1_HandleReceivedByte(u8 byte);
/*
* 发送 FF 01 86,将模块切换到问答模式。
* 默认主动上传模式不需要调用此函数;模块重新上电后恢复主动上传模式。
*/
void Y01_3IN1_RequestMeasurement(void);
#endif
七、实物演示与实验现象
7.1 上电预热现象
气敏传感器需要预热,模块建议预热至少 2 分钟,教学实验建议预热 5‑10 分钟再做实验。上电初期的数据只做显示,不能当作稳定测量结果。
OLED 开机等待界面:
cpp
Y01 AIR QUALITY
WARMING UP...
WAITING FOR DATA
USART3 PB10/PB11
收到第一帧合法数据后切换显示界面:
cpp
Y01 AIR QUALITY
TVOC:0.005mg/m3
CO2 :364ppm
HCHO:0.001mg/m3

小提示:帧顺序协议内部是 TVOC、HCHO、CO₂,屏幕为阅读友好调整为 TVOC、CO₂、HCHO,只是显示排版改变,解析逻辑不变。
正常工作状态,屏幕大约每秒刷新一次。可以通过调试器观察变量valid_frames有效帧计数,如果计数每秒 + 1,代表串口通信完全正常。
⚠️安全提醒:远离明火,禁止液体淋到传感器,不要长时间高浓度熏蒸传感器。该实验演示的是气敏元件交叉响应特性,不是精准定量测量。
7.3 故障排查表
| 故障现象 | 观察现象 | 排查方案 |
|---|---|---|
| OLED 一直显示 WAITING FOR DATA | 接收字节计数始终为 0 | 检查 Y01 模块 5V 供电、GND 共地、B 引脚接 PB11,确认 USART3 初始化 |
| 收到字节,但是没有有效帧 | valid_frames 始终为 0 | 确认波特率 9600 8N1,A/B 引脚没有接反,排查电平问题 |
| checksum_errors 持续上涨 | 大量校验失败 | 检查共地,缩短杜邦线,电源滤波,排查干扰 |
| 收到一帧后不再更新 | valid_frames 不再增加 | 是否误发送命令切到问答模式;检查 B 引脚是否松动 |
| 数值不变,但是 valid_frames 每秒 + 1 | 通信一切正常 | 当前环境气体稳定;做挥发物实验验证传感器响应,不是程序卡死 |
| 对着吹气 CO₂读数几乎不变 | 帧计数正常 | CO₂是推算参考值,非红外传感器,对人体呼气不敏感,属于正常现象 |
| TVOC/HCHO/CO₂三个读数一起跳动 | 帧计数正常 | 广谱传感器交叉干扰,属于模块本身特性 |
八、总结
- Y01‑3IN1 属于串口输出空气质量模块,对比模拟传感器,硬件上省去 ADC,软件重点是帧同步、累加校验和、高低字节拼接解析。
- 使用串口中断 + 状态机,实现字节流识别完整数据帧,保证 OLED 界面不会被串口接收阻塞。
- 一定要区分:模块 CO₂、HCHO 是基于 TVOC 广谱传感器算法推算的参考值,仅适合教学实验看趋势,不能当作专业检测仪。
- 硬件调试最容易踩坑:串口收发接反、模块没有共地、误切换问答模式。
一句话回顾整个项目:Y01‑3in1 感知环境气体变化生成 TVOC、HCHO、CO₂数字帧,UART 主动上报;STM32 通过 USART3 中断完成帧同步校验解析,最后 OLED 展示测量数据。