1. 项目概述与硬件准备
先说结论:Y01-3IN1这个模块加上一块0.96寸OLED屏幕,用STM32F103C8T6就能搭出一个实时空气质量监测小站,成本控制在40块钱以内。整条链路不复杂——传感器模块负责采集数据,STM32通过串口把数据读回来,解析出PM2.5、PM10、温湿度和TVOC,再扔给OLED显示。做完这个小项目,你对STM32的串口通信、中断接收、I2C外设驱动这些基本功都会有一个很扎实的掌握。
这个项目最适合三类人:一是准备做课程设计或毕业设计的电子类学生,二是刚学完STM32基础想练手进阶的嵌入式爱好者,三是想给家里或者工位做个桌面空气质量监测仪、但不想买成品的人。整个教程不会涉及太深的理论,但我会把从硬件接线到代码实现的每一个环节都讲透,尤其是那些容易踩坑的细节。
硬件清单如下:
| 器件 | 型号/规格 | 说明 |
|---|---|---|
| 主控 | STM32F103C8T6 最小系统板 | 最常用的蓝色Pill板,性价比高 |
| 空气质量模块 | Y01-3IN1 | 集成了粉尘、温湿度、TVOC三类传感 |
| 显示屏 | 0.96寸 OLED,SSD1306控制芯片,I2C接口 | 4针,VCC/GND/SCL/SDA |
| USB转TTL | CH340模块 | 用来查看调试信息(可选) |
| 杜邦线 | 公对母若干 | 接线方便 |
| 面包板 | 830孔 | 方便原型搭建 |
Y01-3IN1模块需要额外说明一下。市面上这类“三合一”空气质量模块有好几个版本,有的叫Y01-3IN1,有的叫SGP30+粉尘模块组合,本质上都是把PM2.5/PM10颗粒物传感器、温湿度传感器、TVOC气体传感器集成在一块板子上,通过一个串口输出所有数据。我用的这个模块,粉尘部分基于激光散射原理,可以测PM2.5和PM10;温湿度部分是板载SHT系列芯片;TVOC部分用的是金属氧化物半导体传感器,能测出装修污染、烟雾等挥发性有机物的大致浓度。这类模块省去了自己焊接多个传感器再拼数据的麻烦,非常适合快速原型验证。
1.1 引脚分配与接线方案
接线是整个项目里最容易出错的一步,先看引脚定义。STM32F103C8T6的USART1默认在PA9(TX)和PA10(RX),I2C1的SCL和SDA在PB6和PB7。这里有个非常关键的细节:Y01-3IN1模块的TXD要接STM32的RX,RXD要接STM32的TX,也就是交叉连接,新手最容易在这一步接反。
| STM32引脚 | 外设 | 接Y01-3IN1 | 接OLED |
|---|---|---|---|
| PA9 (USART1_TX) | 串口1 | RXD | - |
| PA10 (USART1_RX) | 串口1 | TXD | - |
| PB6 (I2C1_SCL) | I2C1 | - | SCL |
| PB7 (I2C1_SDA) | I2C1 | - | SDA |
| 5V 或 3.3V | 电源 | VCC | VCC |
| GND | 地 | GND | GND |
供电这里我要多说一句。Y01-3IN1模块的粉尘传感器内部有一个小型风扇或激光发射管,启动瞬间电流比正常工作时大不少。如果你用的是ST-Link或者板载调试器供电,遇到模块屏幕一亮就复位、或者数据跳动很大的情况,第一反应应该是量一下供电电压是否被拉低了。我实测下来,用USB口经过稳压后的5V供电给模块,STM32则用AMS1117出来的3.3V独立供电,整条链路最稳定。OLED的VCC可以接3.3V,因为SSD1306本身工作电压就是1.65V到3.3V,接5V虽然不会立刻烧,但长期运行有风险。
1.2 开发环境与工程模板
开发环境我推荐用Keil MDK 5稳定版配合STM32CubeMX生成初始化代码。我知道很多人还在纠结标准库和HAL库怎么选,我的建议是:新项目直接用HAL库,原因有两点。第一,ST官方已经停止标准库的技术支持,新芯片都只有HAL库;第二,CubeMX生成的代码骨架非常规范,能帮你少写很多外设初始化代码,让你把精力集中在业务逻辑上。如果你手头只有标准库的工程模板,后面我会把UART和I2C的初始化代码也贴出来,照着改也能跑。
2. Y01-3IN1串口协议拆解
驱动任何传感器,第一步不是写代码,而是先把它的通信协议看懂。Y01-3IN1模块的串口参数是固定的:波特率9600,8位数据位,1位停止位,无校验位。模块上电后会主动上报数据,也可以由主机发指令查询。为了方便演示,我采用主机主动查询的方式,这样程序逻辑更可控,OLED上的数据刷新节奏也好掌握。
2.1 模块返回的原始帧分析
模块返回的数据帧格式固定为9个字节,结构如下:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | 0xFF | 帧头 |
| 1 | 0x01 | 模块地址 |
| 2 | 0x86 | 指令 |
| 3 | 数据高位 | PM2.5浓度(ug/m3) |
| 4 | 数据低位 | PM2.5浓度(ug/m3) |
| 5 | 数据高位 | PM10浓度(ug/m3) |
| 6 | 数据低位 | PM10浓度(ug/m3) |
| 7 | 状态位 | 保留,通常为0 |
| 8 | 校验和 | 前7个字节累加和的低8位 |
这个帧格式其实是很多空气检测模块通用的方案。数据都是大端模式,也就是先高字节后低字节。比如PM2.5浓度是0x01 0xA5,换算出来的十进制就是421,代表当前PM2.5浓度是421微克每立方米。状态位这个字节在不同批次模块里含义不一样,有的返回0,有的返回传感器内部状态码,我们日常使用可以忽略它。校验和的计算方法是:把第0到第6字节的数字加起来,取结果的低8位。比如前面几个字节是FF 01 86 01 A5 00 2B,累加得到0xFF + 0x01 + 0x86 + 0x01 + 0xA5 + 0x00 + 0x2B = 0x261,低8位就是0x61,那第8字节就应该是0x61。
主机查询指令是:FF 01 86 00 00 00 00 00 79。注意最后两位的校验和,前7字节累加是FF+01+86+00+00+00+00 = 0x186,低8位是0x86,但指令第8位却是0x79。这说明这类模块用的是自定义校验算法而非简单累加,所以我强烈建议你只用文档提供的现成查询帧,不要自己推导。这个教训我一开始也踩过,后来看了芯片手册才发现模块内置了私有校验逻辑。
2.2 解析逻辑的代码实现思路
解析串口数据最忌讳的做法是“来一个字节读一个字节”。正确做法是把数据先收进一个缓冲区,当收满9个字节后,再统一进行校验和判断。我们先校验帧头0xFF、地址0x01、指令0x86,再判断第8字节是否等于前7个字节累加和的低8位,都通过了才提取数据。这样能最大程度避免因为电磁干扰导致的错误数据。
串口接收方式上,我推荐用中断接收而不是阻塞式等待,因为主循环还要刷OLED,阻塞等待会把整个系统卡死。用HAL库的做法是:在初始化时调用HAL_UART_Receive_IT(&huart1, &rx_buffer, 1),让串口每次只接收单个字节并触发中断回调,然后在回调函数里自己写一个状态机来拼接一个完整的9字节帧。虽然看起来多了一步,但这是嵌入式串口数据接收最实用、最健壮的套路。
3. CubeMX工程搭建与核心配置
开始写代码之前,先用CubeMX把底层的硬件初始化搞利索。用CubeMX的好处是引脚冲突、时钟树错误这些问题它会直接提示,不会等到烧录了才发现。
3.1 时钟树与调试接口配置
打开CubeMX后,芯片型号选STM32F103C8Tx。在SYS选项卡里,Debug选项一定要选Serial Wire,否则烧录一次后ST-Link就找不到芯片了。这一点是我见过出现频率最高的初学者问题,烧录时提示“no stm32 target found”,十有八九就是Debug配置没设对。
时钟树配置选HSE外部晶振,把主频拉到72MHz,这是F103的极限频率。操作路径:RCC → HSE选择Crystal/Ceramic Resonator,然后在Clock Configuration页面把HCLK改成72。如果用的是内部RC时钟(HSI),也能跑,但串口波特率的误差会大不少,9600波特率下虽然还能容忍,但不推荐。外部晶振才是稳定之选。
3.2 USART1与I2C1参数配置
UART配置参数如下:
| 参数 | 值 |
|---|---|
| Mode | Asynchronous |
| Baud Rate | 9600 |
| Word Length | 8 Bits |
| Parity | None |
| Stop Bits | 1 |
| USART1 中断 | 开启(NVIC里勾选 USART1 global interrupt) |
I2C配置比较简单,Mode选I2C,默认100KHz标准模式即可。SSD1306这颗OLED控制芯片本身对时序要求不苛刻,100KHz和400KHz都能稳定工作。我们保持默认100K就好,没必要为了快那么一点去冒险。NVIC设置里I2C1的全局中断也可以顺手开一下,虽然用轮询方式驱动OLED时不太需要,但后续如果改成中断方式SLAVE通信,会省不少事。
生成工程时,Toolchain选MDK-ARM,Code Generator里把“Generate peripheral initialization as a pair of .c/.h files”勾上,这样每个外设单独一个文件,可读性更好。另外在Advanced settings里,把USART1和I2C1的初始化函数都放在main.c的初始化列表前面,方便统筹查看。
3.3 生成后的工程结构解读
生成的工程里,MX_GPIO_Init()会初始化引脚,MX_USART1_UART_Init()配置串口,MX_I2C1_Init()配置I2C。主循环里目前只有一个空的while(1)。我们的代码要添加的文件包括:OLED驱动文件(oled.c/oled.h)、数据解析文件(aq_data.c/aq_data.h)。整体分层是:底层外设(CubeMX生成)→ 硬件驱动层(OLED、传感器模块)→ 应用层(主逻辑)。
4. 驱动代码编写与核心逻辑实现
下面进入正题。代码部分我按照“OLED驱动、串口接收解析、主循环整合”三个模块逐个说明。
4.1 OLED驱动移植与初始化流程
SSD1306的OLED驱动网上的代码很多,核心就是控制芯片的初始化序列和写入方式。我用的驱动文件结构大概是这样的:
oled.c里面主要包含以下接口:
OLED_Init():发送初始化命令序列OLED_Clear():把显存全部清零OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size):显示字符串OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t no, uint8_t size):显示汉字
关键点是OLED的写入方式。SSD1306内部有一块显存,我们操作显存有两种思路:一种是直接写数据到DDRAM,另一种是在单片机内存里维护一块显存副本,全部修改完成后一次性刷新到OLED。后者叫“双缓冲”,好处是不会闪烁。我移植的驱动就用了128*8字节的显存数组,每个像素对应一位,修改后调用OLED_Refresh()把整个显存通过I2C批量写入。
I2C写OLED寄存器的时序分为两步:先发送控制字节,0x00表示后续为命令,0x40表示后续为数据。OLED的I2C地址默认为0x78(7位地址0x3C左移一位),如果屏幕没反应,先检查是不是买到了0x3D地址的版本。市面上90%的0.96寸OLED都是0x3C,但确实存在0x3D的版本,屏幕背面丝印上一般会标注。
4.2 汉字取模方法与显示技巧
OLED显示数字和英文很简单,标准ASCII字库5x8或者8x16直接查表就行。但显示汉字就得单独处理“取模”这个环节。我用的取模软件是PCtoLCD2002,这个老软件虽然界面朴素,但功能完全够用。取模步骤:
- 打开PCtoLCD2002,选择“字符模式”
- 输入你要显示的汉字,比如“空气质量”
- 字体选宋体或黑体,字号16x16
- 在“选项”里设置取模方式:阴码、逐行式、顺向、C51格式
- 生成点阵数据,复制到代码里
取模格式里的几个术语值得解释一下。“阴码”指有像素的地方是1,无像素是0;“逐行式”指按从左到右、从上到下的顺序取;“顺向”指高位在前;“C51格式”指数据按照C语言的16进制数组格式输出:0x00, 0x1F这样。一个16x16的汉字,需要32个字节的数据。做空气质量显示界面时,我建议第一行放“PM2.5: 012 ug/m3”,第二行放“PM10: 034 ug/m3”,第三行放“TEMP: 26.5 C”,第四行放“TVOC: 0.12 mg/m3”。每行高度16像素,4行刚好占满128x64的屏幕。
这里提醒一句:OLED屏幕在纯图形模式下,像素点由多层结构组成,驱动控制器负责把显存中的“1”映射为点亮、“0”映射为熄灭,所以取模方向错了,字显示出来就是镜像或者倒置的,遇到这种情况不要怀疑屏幕坏了,去检查取模设置。
4.3 串口接收状态机的完整实现
串口接收的核心代码,我用HAL库的中断回调加状态机来写。思路是:每收到一个字节,就根据当前状态判断它是不是帧头、地址、指令……一步步填充缓冲数组。
/* aq_data.c */ #define FRAME_SIZE 9 uint8_t uart_rx_byte = 0; uint8_t rx_frame[FRAME_SIZE]; uint8_t frame_index = 0; volatile uint8_t frame_ready = 0; typedef enum { WAIT_HEAD = 0, WAIT_ADDR, WAIT_CMD, WAIT_DATA, } parse_state_t; parse_state_t parse_state = WAIT_HEAD; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { switch (parse_state) { case WAIT_HEAD: if (uart_rx_byte == 0xFF) { rx_frame[0] = uart_rx_byte; parse_state = WAIT_ADDR; } break; case WAIT_ADDR: if (uart_rx_byte == 0x01) { rx_frame[1] = uart_rx_byte; parse_state = WAIT_CMD; } else { parse_state = WAIT_HEAD; // 地址不对,重新等帧头 } break; case WAIT_CMD: if (uart_rx_byte == 0x86) { rx_frame[2] = uart_rx_byte; frame_index = 3; parse_state = WAIT_DATA; } else { parse_state = WAIT_HEAD; } break; case WAIT_DATA: rx_frame[frame_index++] = uart_rx_byte; if (frame_index >= FRAME_SIZE) { frame_ready = 1; parse_state = WAIT_HEAD; } break; } HAL_UART_Receive_IT(&huart1, &uart_rx_byte, 1); } }这个状态机看起来很基础,但非常可靠。它解决了一个经典难题:串口数据流一旦中间丢了一个字节,后面全乱。通过状态机逐字节校验帧头、地址、指令,即使中间断流,也能在下一帧重新同步。每次进入中断都会重新调用HAL_UART_Receive_IT,保证串口始终处于接收状态。
需要特别说明的是,HAL库的HAL_UART_Receive_IT在接收完成一次后,会自动关闭接收,所以必须在回调里再次开启,否则只会收到一帧数据就停住。很多初学者在这里卡住,症状是程序烧进去屏幕正常但数值永远不变,就是这个原因。
4.4 数据校验与数值提取
当frame_ready置1后,在主循环里做一个校验,通过之后才更新全局变量。校验函数如下:
/* aq_data.c */ uint8_t AQ_GetCheckSum(uint8_t *buf) { uint8_t sum = 0; for (int i = 0; i < 7; i++) { sum += buf[i]; } return sum; } void AQ_ParseFrame(uint8_t *buf) { if (buf[0] != 0xFF || buf[1] != 0x01 || buf[2] != 0x86) { return; } if (AQ_GetCheckSum(buf) != buf[8]) { return; } pm25 = (uint16_t)(buf[3] << 8) | buf[4]; pm10 = (uint16_t)(buf[5] << 8) | buf[6]; }这个设计把“接收”和“解析”分开,中断只负责收数据,主循环负责校验和提取,避免在中断里做太多运算。PM2.5和PM10的数据都是16位,分别放在两个字节里,所以要把高字节左移8位再和低字节按位或。有人喜欢用buf[3] * 256 + buf[4],效果一样,但移位写法更贴近硬件思维。
4.5 主循环与显示刷新逻辑
主循环的设计要把握一个原则:不要每圈都发查询指令,也不要每圈都刷OLED。传感器数据变化不会那么快,过高频率的查询只会增加模块负担,甚至导致串口数据混乱。我的做法是:每2秒发一次查询指令,收到有效数据后刷新一次OLED,中间的时间主循环空转等待。如果连续5秒没收到有效数据,OLED上显示“- -”表示离线。
/* main.c while(1) 中 */ uint32_t last_query_ms = 0; uint32_t last_refresh_ms = 0; while (1) { if (HAL_GetTick() - last_query_ms >= 2000) { uint8_t cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; HAL_UART_Transmit(&huart1, cmd, 9, 100); last_query_ms = HAL_GetTick(); } if (frame_ready) { AQ_ParseFrame(rx_frame); frame_ready = 0; last_refresh_ms = HAL_GetTick(); } if (HAL_GetTick() - last_refresh_ms > 5000) { OLED_ShowString(8, 16, "NO SIGNAL", 16); } }这里用HAL_GetTick()做非阻塞延时替代HAL_Delay()。为什么不用HAL_Delay()?因为如果代码里任何一处用了阻塞延时,串口中断照样能进,但主循环在延时期间干不了别的活,OLED刷新、按键检测这些都会被卡住。用时间戳的方式判断“是否到时间了”,程序看起来是轮询,实际上是变相的多任务调度,这是嵌入式开发里特别基础又特别重要的思想。
5. 常见问题与排查技巧实录
项目做完了,运行过程中总会遇到各种幺蛾子。我把实操中最常碰到的问题整理成了一张排查表,按出现频率排序。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 烧录一次后ST-Link找不到芯片 | CubeMX里Debug模式没选Serial Wire | 按住开发板复位键再烧录;重新配置Debug后再烧录 |
| OLED不亮或纯蓝屏 | I2C地址错误或接线松脱 | 确认是0x3C还是0x3D;重新插拔SCL/SDA |
| OLED有背光但不显示内容 | 初始化时序问题 | 检查OLED_Init是否在I2C初始化之后调用 |
| 串口收不到数据 | TX/RX接反或波特率错误 | 交叉接线;确认波特率9600 |
| 数据偶尔清零或乱跳 | 供电电压不稳定 | 模块独立5V供电;加100uF电解电容 |
| 汉字显示成乱码 | 取模方向设置错误 | 重新取模,用“阴码、逐行、顺向” |
| 显示的数字一直不变 | HAL_UART_Receive_IT没重新开启 | 在回调函数末尾再次调用接收函数 |
5.1 针对“no stm32 target found”的深入排查
这个报错在网络热词里出现过,也是我当年入坑时遇到的头号问题。它提示的信息很明确——调试器连不上芯片。原因通常有三个:
第一,CubeMX生成工程时Debug没选Serial Wire,导致烧录完成后PA13/PA14被配置成普通GPIO,调试口被关掉了。解决办法:按住开发板上的复位键不松开,点击Keil里的Download按钮,然后瞬间松开复位键,这样能抢在程序启动前把调试器连上,烧录一次修复后的固件就能永久解决。
第二,连接线接触不良。ST-Link的SWDIO和SWCLK线不要超过20厘米,线越短越稳定。杜邦线氧化或者松动也会导致连接不上,重新插拔可能就好了。
第三,芯片被低功耗模式锁死。如果程序里进了STOP模式,调试口也会掉线,处理方法和第一种一样,按住复位抢烧。
5.2 串口数据不对的现场排障思路
如果OLED能点亮但显示的数值永远不变,或者数值在0和实际值之间乱跳,优先怀疑串口数据质量。我先用USB转TTL模块把Y01-3IN1的TXD直接接到电脑串口助手,看模块本身有没有正常输出原始数据。这一步能快速定位问题在模块还是单片机,是排障里最有效的手段。
串口助手显示正常的原始帧之后,再把USART1的TX通过USB转TTL连到电脑(注意共地),在代码里把主循环收到的原始帧头几个字节打印出来。如果打印出来是FF 01 86开头的,说明连线没问题;如果出现FF 00 86或者FF 01 00之类,说明中间有字节丢失。这时候优先检查波特率误差和时钟配置。内部RC和外部晶振的误差在9600波特率下都不明显,但如果你把波特率调高了,晶振不准的问题就会被放大。
还有一个坑是模块和STM32的GND没连上。TXD和RXD虽然接了,但两个设备的地电位不一致,串口通信就会丢字节甚至完全不通。手动接一根地线,大多数奇怪问题都能解决。
5.3 OLED显示异常的集中分析
OLED屏幕如果只亮背光、没有内容,绝大多数情况是初始化失败。初始化必须在I2C外设已经使能之后调用,顺序反了就会收到NACK。另外,I2C总线上拉了上拉电阻,如果你的模块板上没有4.7K上拉,需要自己外接。我们的OLED模块大多数都自带上拉,但如果你自己飞线接传感器,可能就需要手动补。
我还遇到过一种情况:OLED能显示但每隔几秒闪烁一次。这个不是屏幕问题,而是显存刷新被串口中断频繁打断。因为OLED刷新函数内部有多条I2C写操作,如果串口数据持续涌入,会在刷新过程中反复抢占CPU,导致刷新节奏不稳定。解决办法是合理设计刷新频率,不要每收到一帧就刷一次屏,提供一个“数据变化超过阈值才刷新”或者“固定500ms刷新一次”的策略,闪烁问题基本能消除。
6. 扩展思路:从单机监测到多节点组网
做完基础版之后,很多朋友会问下一步能玩什么。其实这个项目的扩展空间非常大,我简单说几个方向,给有进阶需求的读者一个参考。
第一个方向是加LCD显示换成TFT屏或者墨水屏。OLED的优点是亮度高、刷新快、功耗低,缺点是尺寸小、信息量有限。如果你想把历史曲线画出来,比如PM2.5过去24小时的变化趋势,可以考虑换一块1.8寸TFT屏,用SPI接口驱动,刷新率更高,显示内容更多。我在后续改进版里就换成了ST7735的TFT屏,UI设计空间大了很多,能同时显示波形和数字。
第二个方向是增加存储功能。给STM32外挂一个SD卡模块或者SPI Flash芯片,把每小时的数据记录下来,配合RTC时钟芯片打上时间戳。这样你出门一天回来,插上SD卡就能用电脑整理出一份空气质量日报。对室内空气质量检测这种需要长时间观测的场景,这个功能特别实用。
第三个方向是联网上报。Y01-3IN1把数据读出来之后,通过ESP8266或ESP32的WiFi能力,把数据POST到本地MQTT服务器或者第三方平台的物联网服务,手机App实时查看。这个方案需要处理TCP协议栈,但我强烈建议先从ESP8266的透传模式玩起——AT指令配好网络,STM32只管把串口数据转发过去,逻辑简单,成功率极高。如果用的是ESP32,甚至可以跳过STM32,直接用ESP32的UART接模块,MicroPython和Arduino框架都支持,开发效率提升一个数量级。不过那就是另一个项目了,本教程的核心——串口解析、数据校验、OLED驱动这些基本功,到哪个平台都一样通用。
第四个方向是设备联动,这也是我最推荐的一个进阶玩法。空气质量数据不是光看就完了,结合继电器模块,当TVOC浓度超标时自动开启空气净化器;当温湿度高于设定阈值时,自动打开风扇;搭配蜂鸣器,检测到PM2.5爆表时发出报警提示。这些逻辑都是在现有代码基础上增加几个GPIO控制,MCU内部增加几个阈值判断函数就行。我在实际改造中,用STM32的一个串口接收传感器数据,三个引脚分别控制风扇、加湿器、蜂鸣器,整体成本不超过80元,效果却很直观。
7. 最后的实操心得与建议
这个项目虽然入门,但从零到最终稳定运行,我前前后后调了一整天。回头复盘,最值得记住的几点经验:
数据解析一定要分层。中断只负责收字节,主循环负责校验,界面只负责显示,三个模块各干各的活。这样出了问题定位非常快,串口没数据就查接收层,有数据但不对就查解析层。一上来就想写一个函数串起所有功能,调试的时候会很痛苦。
模块数据不稳定的时候,先怀疑供电再怀疑算法。我以前用模块的3.3V供电,PM2.5数据跳动很厉害,一度以为是通信干扰,甚至去改代码滤波逻辑。后来用示波器量了电源轨,发现纹波有200mV。换成独立5V供电之后,数据稳如老狗。这也解释了为什么很多传感器模块的数据手册上要强调“电源质量直接影响测量精度”——物理层面的因素首先排除,再碰代码。
调试阶段可以多利用串口打印。OLED屏幕地方小,不适合显示冗长的调试信息,我习惯把所有原始帧通过USART2打印到串口助手。这样做的好处是能看到完整的数据流,比如有没有丢字节、校验和不匹配的频率有多高,这些信息比只看最终结果有用得多。项目稳定后再把调试打印关掉,释放CPU资源。
最后想说说关于芯片选型的一点体会。F103C8T6这颗芯片虽然老,但生态极其成熟,网上资料多到看不完,对初学者非常友好。等哪天你发现它的Flash和RAM不够用了,再往上跳到F407或者F103ZET6,代码几乎不用改动太多,这就是平台的积累优势。希望这篇教程能帮你把STM32+传感器+OLED这条最经典的嵌入式开发链路跑通,后面再接触更复杂的项目,你会发现很多套路都是相通的。