玩转STM32:从0到1搭建心率血氧手环(GPS版)全记录
前两天整理开发箱,翻出一块吃灰许久的开发板,刚好手边又凑齐了几颗传感器模块,于是琢磨着搞个像样的穿戴设备出来。想着一路做下来,交互、传感器、定位、低功耗、数据结构全都能覆盖到,最后就定下了“STM32心率血氧手环(GPS版)”这个方向。如果你也想练手单片机综合项目,或者正在找毕设/竞赛的题材,这篇东西应该能帮你在动手前把整个脉络捋清楚,少走不少弯路。
1. 项目整体规划与需求拆解
很多人做项目习惯拿到板子就开写代码,结果做到一半发现引脚不够用、功耗压不下去、模块互相干扰,回头再返工,特别打击积极性。这类综合型项目,第一步一定不是敲代码,而是把“这个手环到底要干什么”想明白。
1.1 核心功能定位
从标题拆解,这个项目包含三个核心关键词:心率血氧、手环、GPS。分别对应三类需求:
- 生理监测:实时采集心率(HR)和血氧饱和度(SpO2),这是手环的“灵魂功能”,需要稳定的光学传感器和一套能扛住运动干扰的信号处理逻辑。
- 便携形态:手环意味着小尺寸、电池供电、轻量化交互,显示、震动、按键等交互模块都得有,而且不能太耗电。
- 位置定位:GPS模块负责获取经纬度、速度和UTC时间,可以用在户外运动记录、轨迹回放、紧急定位等场景。
这三个功能凑一起,项目气质一下就出来了——这不是一个“点灯实验”,而是一个需要软硬协同、资源有限的小型嵌入式终端。
1.2 硬件选型的底层逻辑
先说主控芯片。GPS手环对主控的要求是:外设接口够用(I2C、UART、SPI)、主频不用太高(因为传感器数据量不大)、功耗尽量低(电池就那么大)。我用的是一块基于某款Cortex-M4内核的国产MCU,72MHz主频、内置128KB Flash和20KB SRAM,外设资源充足,关键是买得到、便宜、资料全。你用经典的STM32F103C8T6也完全可行,只是低功耗表现和片上资源会比M4内核的稍弱一些,后面讲功耗时会具体说明差异。
传感器选型上,心率血氧基本就是标准答案了。我选的是那颗在可穿戴圈子里被用滥了的光学传感器,集成了红光和红外光LED、光电二极管、模数转换和片上算法,利用光电容积描记法原理实现测量,通过I2C接口直接输出数字信号。别嫌它老,稳定性、资料完整度、价廉程度都吊打各种杂牌方案。
GPS模块用的是国产的ATGM336H,串口输出NMEA协议,冷启动定位时间大概在30~60秒,配合无源天线就能工作。屏幕方面,我选了一块0.96寸的OLED,SSD1306控制器,I2C接口,分辨率128x64,显示时间、心率波形、定位状态都够用。
其实整个选型逻辑就是一条线:手环形态决定功耗约束,功耗约束决定主控型号,主控型号决定外设选型范围,倒着推一遍,方案自然就清晰了。
1.3 整体架构与数据流
整个系统的工作流是这样的:
传感器采集原始光信号 → MCU通过I2C读取数据 → 算法计算出血氧和心率 → 数据实时显示在OLED上 → 周期性存储到Flash外部芯片 → GPS数据通过串口进入MCU → 数据叠加显示并存储。
往外扩展一点,如果有蓝牙模块(比如HC-08或者BLE芯片),还可以把数据打包上传手机APP。我这次做的版本没有加蓝牙,但预留了串口调试接口,方便后续接无线模块或者用串口把数据传到电脑上做波形分析。
2. 心率血氧检测的原理与采集细节
心率血氧听着高大上,核心原理其实挺朴素。这里值得花点笔墨讲明白,因为算法参数调整、滤波设计全都依赖于对原理的理解,只背代码是搞不定实际问题的。
2.1 光电容积描记法到底是怎么回事
我们的皮肤组织、血液、骨骼对光的吸收特性各不相同。血液中氧合血红蛋白和还原血红蛋白对红光和红外光的吸收系数有明显差异:氧合血红蛋白对红外光吸收更强,还原血红蛋白对红光吸收更强。
传感器工作时,红光LED和红外光LED交替点亮,光线穿透皮肤(指尖、手腕等组织较薄的部位)后被光电二极管接收。由于动脉血随心跳搏动,血管内血流量周期性变化,导致接收到的光强也随之波动。于是,光电二极管输出的电信号中就包含了:
- 直流分量:由皮肤、肌肉、静脉血等静态组织吸收产生,变化缓慢。
- 交流分量:由动脉搏动引起,频率与心跳同步,幅度只有直流分量的1%~2%。
提取出交流分量的频率,就能算出心率;计算红光和红外光交流分量幅度的比值(R值),通过经验拟合曲线就能估算出血氧饱和度。这就是PPG信号的完整逻辑链。
2.2 传感器配置与实际测量
传感器模块是通过I2C接口通信的,基于这颗传感器的寄存器配置,有几个关键参数需要注意:
- LED电流:决定了光信号的强度。默认配置下红光和红外光的电流通常设置为两个量级,但实际佩戴后要测试一下采样值是否处于合理区间,大概在几千到数万(Low端)范围,如果饱和或者太暗都要调整。红外和红光电流可以分开配置,这很关键,因为不同肤色、不同厚度的手腕对光的吸收差异很大。
- 采样率:典型的ADC采样率可以设成100Hz或者50Hz,对心率检测来说够用了。采样率太低会导致波形失真,心率算法的结果就不准。
- ADC量程:一般选16位模式,动态范围更充裕。
实际连接时,I2C上拉电阻很重要,很多人的传感器读数偶尔飘一下,几乎都是因为模块自带的上拉电阻太弱,或者接线太长导致总线电容过大。我试过在SCL和SDA线上各加一个4.7kΩ上拉电阻到3.3V,稳定性立竿见影。
2.3 心率与血氧算法实操
传感器硬件能拿到原始PPG波形,但原始波形噪声很大。手腕部位测量比指尖测量的信噪比低不少,因为手腕组织更厚、血流信号弱、运动伪差明显。所以算法层面需要做这几件事:
第一步:带通滤波
心率信号的有效频率范围大约是0.5Hz~3.3Hz(对应30~200次/分钟),所以需要设计一个带通滤波器,把直流漂移、工频干扰、高频噪声滤掉。我在MCU上实现的是一个一阶高通滤波器串联一个一阶低通滤波器。代码示例:
// 高通滤波,截止频率约0.5Hz float alpha_hp = 0.95f; // 根据采样率调整 filtered_hp = alpha_hp * (filtered_hp + raw_data - prev_data); prev_data = raw_data; // 低通滤波,截止频率约3.5Hz float alpha_lp = 0.05f; // 根据采样率调整 filtered = filtered + alpha_lp * (filtered_hp - filtered);第二步:峰值检测与心率计算
滤波后的波形相对干净,接下来就是找波峰。我用的是动态阈值法:维护一个滑动窗口,实时更新波形的最大值和最小值,阈值取二者中点,当信号从低于阈值穿越到高于阈值并且保持一定时间间隔时,判定为一个心跳周期。测出相邻两个波峰的时间间隔,换算成每分钟心跳次数即可。为了避免误检,连续记录的RR间隔应该加一个范围约束,比如0.3s~2s之间才判定有效。
第三步:血氧饱和度计算
血氧计算需要同时处理红光和红外两路信号。对每一路分别提取交流分量和直流分量,算出比值R:
R = (AC_red / DC_red) / (AC_ir / DC_ir)然后通过经验公式拟合SpO2。我的做法是建立了一个简单的查找表,把R值映射到血氧百分比:
// 简化后的R值到SpO2映射 // R: 0.4 -> SpO2 98%, R: 1.0 -> SpO2 90%, R: 1.8 -> SpO2 80% float spo2 = 110.0f - 25.0f * R; if (spo2 > 100) spo2 = 100; if (spo2 < 55) spo2 = 55;实测下来这个简化线性关系已经能应付日常监测场景了,误差范围在±2%左右,跟市面上一些消费级手环差不多。医疗级精度不是这种方案能解决的,心里有数就行。
2.4 这个环节常见的坑
传感器最怕的是佩戴松动和运动干扰。模块传感器与皮肤之间必须紧密贴合且无相对滑动,但又不能压得过紧导致血流不畅。我踩过的一个大坑是:静态测量时数据十分稳定,稍微走动几步波形就乱成一片。后来发现是因为腕带没固定好,模块在表带上轻微晃动,光学窗口与皮肤之间产生相对位移,产生了巨大的伪差。解决办法是重新设计外壳固定结构,在传感器背面加了海绵垫,让光学窗口始终贴在皮肤上。
另一个常见问题是外界光干扰。OLED屏幕的光、阳光、灯光都可能直接射入传感器光学窗口,污染PPG信号。PCB布局上光学窗口周围要尽可能做遮光处理,测试时用不透光的胶带或者黑色遮光绵把缝隙堵上,波形会干净很多。
3. GPS定位模块的接入与管理
GPS是这个手环项目区别于一般“心率手表”的核心差异点。但同时,GPS也是最容易把项目带崩的模块,因为它在启动阶段电流大、定位时间长、串口数据长而杂。管理不好,整个系统就会卡顿或者掉电。
3.1 GPS模块选型与硬件接线
我用的GPS模块是基于AT6558方案的国产模组,支持GPS和北斗双星联合定位,串口输出NMEA协议,工作电压3.3V,电流大概在25mA左右,属于比较常规的指标。模块最大的好处是自带天线接口和串口调试引脚,可以直接用USB转串口模块在电脑上配置参数,比如把输出语句精简成只有GPGGA和GPRMC,从默认的每秒好几条减到每秒一条,减少数据冗余。
接线很简单:模块的TXD接MCU的UART_RX引脚,模块的RXD接MCU的UART_TX引脚,VCC接3.3V,GND接GND。注意共地,然后串口波特率我设置为9600bps,这是一个稳妥的选择,抗干扰能力也更好。
天线部分要强调一点:陶瓷无源天线需要一个较大的“净空区”,天线周围不要铺铜、不要走线、不要放金属物体。我最初为了PCB紧凑,把天线紧挨着屏幕FPC排线,结果卫星信号死活收不到,最后把天线移到了手腕外侧边缘,定位才恢复正常。
3.2 NMEA数据解析实战
GPS模块一上电就会持续往外吐数据,其中最关键的两条语句是:
- GPRMC:包含经纬度、速度、日期时间、定位状态标志。
- GPGGA:包含海拔高度、卫星数量、定位质量等。
MCU串口中断收数据,存入环形缓冲区,然后在主循环里按行提取,以$开头、\r\n结尾的一整句为一次解析单位。由于串口数据是断断续续到达的,需要有状态机逐字符处理:检测到$进入接收状态,检测到\n表示一行结束,然后把这行交给解析函数。
GPRMC的标准结构如此:
$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62字段含义依次是:UTC时间(时分秒)、定位状态(A=有效,V=无效)、纬度(度分格式)、南纬/北纬、经度(度分格式)、东经/西经、速度(节)、航向角、UTC日期(日月年)、磁偏角。
解析时需要把“度分”格式转成“度”的小数格式,公式是:度 = 整数部分 + 分钟部分 / 60。写一个简单函数就行:
float convert_to_degrees(float raw) { int degrees = (int)(raw / 100); float minutes = raw - degrees * 100; return degrees + minutes / 60.0f; }判断定位是否有效,必须检查状态标志是否为'A'。有时候模块虽然返回了经纬度,但这个标志是'V',说明信号质量不合格,这种数据直接丢弃,避免把错误位置显示在屏幕上。
3.3 GPS功耗控制的思路
手环电池经不起GPS一直开着。GPS接收机在持续跟踪状态下的功耗为25mA左右,而整个手环在普通工作状态只有几mA,这多出来的25mA就会让续航直接缩水一大截。所以手环形态下必须考虑GPS的间歇工作。
我的方案是:默认状态下GPS模块处于掉电模式,MCU通过IO口控制模块的电源开关或唤醒引脚。只有在用户按下特定按钮、或者在户外运动模式下,才唤醒GPS进行定位。获取有效定位后,每10秒更新一次坐标,连续工作5分钟后自动关闭GPS,回到低功耗模式。这样既能记录运动轨迹,又不会让电池在半天内耗尽。
另外可以养成的习惯是:把GPS开启后的数据存入环形缓冲区,批量存储到外部Flash,避免反复擦写Flash损耗寿命。
4. 手环的系统软件架构与数据管理
当多个外设同时工作的时候,程序架构如果不清晰,就是一场灾难。心率传感器每50ms产生一次数据、GPS串口每秒吐一行NMEA、OLED需要刷新显示、按键需要响应、数据需要写入Flash……如果全都挤在主循环里暴刀轮询,任何一个环节卡顿都会影响其他环节。
4.1 基于状态机的任务管理
我采用的是一种轻量级的状态机架构,不需要RTOS就足够应对这个体量的项目。整体划分为这么几个状态:
- 待机态:屏幕熄灭,心率采样停止,GPS关闭,RTC正常运行,等待按键唤醒。
- 心率监测态:传感器全速运行,数据实时刷新到屏幕,显示心率值和血氧值。
- GPS运动态:GPS模块上电,获取定位并记录轨迹,同时心率传感器继续工作。
- 数据查看态:从Flash中读取历史数据,翻页显示。
每个状态下,程序做的事情完全不同,但结构上是统一的:状态入口执行初始化,状态循环执行本状态的事务,状态退出执行清理。这样的好处是代码逻辑清晰,也不会出现GPS解析过程中插进来一个耗时程序导致串口数据丢失的情况。
按键处理用外部中断,设置防抖定时器,按下后在缓冲区里记录事件,主循环周期性地查询并处理事件。这样可以避免在中断里做耗时的业务逻辑。
4.2 数据存储与历史记录
实时数据如果不存储,手环的实用性会大打折扣。心率血氧是连续产生的,而MCU内部Flash擦写次数有限,不能频繁写入。所以需要外挂一颗Flash存储芯片,容量一般是8Mbit,也就是1MB,用来保存历史数据绰绰有余。
写Flash要注意“先擦后写”的规则。按页擦除、按字节写入,我设计了一段简单的存储管理逻辑:
- 起始地址固定,每页存储一天的记录(每分钟一条心率+血氧+坐标)。
- 写入时先检查页是否满了,满了就擦除下一页。
- 读取时按页偏移顺序读出并解析。
嵌入式系统的断电保护很关键,数据存储过程中意外断电可能导致写坏一整页。稳妥的处理是:每一页开头写一个标识位,读数据时校验标识位和CRC校验值,不合法就跳过这一页。这样即便发生异常断电,也能保证至少不返回错误数据。
4.3 显示与交互的取舍
0.96寸OLED能显示的信息量非常有限,必须做信息分层。我的方案是设计了三屏循环显示:
- 第一屏:心率 + 血氧 + 心率波形小窗。
- 第二屏:GPS坐标 + 海拔 + 速度(如果定位有效)。
- 第三屏:日期时间 + 步数(可以加上一个加速度传感器扩展)。
按键短按切换屏幕,长按进入GPS运动模式,这个交互逻辑简单直接,不增加学习成本。OLED用I2C刷新,整个刷一帧大约需要几十毫秒,如果刷新频率太高反而会闪烁,实测10Hz刷新率就好,不影响观看而且能减少主控负担。
4.4 功耗调试的实操经验
这是整个项目最磨人的环节。我遇到了一个很典型的功耗问题:用万用表测量,系统在“待机态”电流高达6mA,远低于预期,但屏幕明明已经熄灭了。排查了一圈发现,罪魁祸首是GPIO悬空。某个进入外部中断的按键引脚在休眠状态下没有配置上拉电阻,导致引脚悬浮,不断产生边沿触发,MCU频繁被唤醒,电流自然降不下去。
后来把所有处于空闲状态的引脚统一配置为模拟输入或者带上拉的推挽输出低电平,待机电流终于降到了几百微安级别。另外,OLED模块本身在静态显示时也会耗电,需要为它单独加一个P-MOS开关,待机时直接切断电源,不搞断电,电流立刻又降了一块。
5. 实测数据与问题排查记录
项目做到80%进度的时候,才是真正学到东西的时间。这里把我在调试阶段遇到的最有价值的几个问题记录下来,希望能帮你提前绕开这些坑。
5.1 问题一:心率数值偶发跳变到200+
现象描述:静坐状态下手环显示的心率在70左右,但每隔几分钟会突然跳到190,过一会儿又恢复正常。
排查过程:先用串口把原始PPG波形导出来观察,发现波峰检测算法把T波误判成了主波峰。因为滤波后的信号不是完美的正弦波,波形会有小幅次级波,如果动态阈值设置太低,就会导致一个周期内检测到两个波峰。此外,如果在波形处于峰值附近时出现轻微干扰,会有额外的毛刺峰。
解决方案:在峰值检测逻辑中加上“不应期”限制,即检测到一个有效波峰后,至少在250ms内不再接受新的波峰。这个时间对应心率上限约240次/分钟,锁死之后,误检率明显下降。
5.2 问题二:GPS时常显示无效定位
现象描述:在室外开阔地测试,GPS模块上电很久都无法锁定,偶尔锁定几秒又丢失。
排查过程:一开始怀疑是天线质量或者模块供电不足,用示波器检查模块电源,发现纹波将近100mV。GPS接收机对电源纹波很敏感,尤其是陶瓷天线和射频前端部分,纹波会影响接收灵敏度。又检查了天线馈点,发现手工焊接的焊点不饱满,内芯接触不良。
解决方案:在GPS模块电源引脚并联了一个10uF的钽电容和100nF的陶瓷电容,抑制纹波;重新焊接天线座,确保接触可靠。更换后冷启动定位时间缩短到30秒左右,信号稳定度也明显提升。
5.3 问题三:OLED屏幕偶发花屏
现象描述:系统运行一段时间后,屏幕出现雪花点或者整块颜色错乱,断电重启恢复。
排查过程:这个问题的根源在I2C通信异常。在GPS解析器运行期间,主循环有一次耗时的运算(比如浮点数转换),导致I2C数据传输被中途打断,总线时序错乱,OLED控制器进入了非正常状态。
解决方案:OLED刷新函数内部加超时机制和总线错误恢复,检测到I2C总线忙超时后,重新初始化OLED控制器。同时在GPS解析处理中避免不必要的浮点运算,能用整数计算的就不用浮点。这个经验提醒我,在嵌入式裸机环境中,任何耗时操作都可能干扰外设时序,必须时刻注意模块间的隔离。
5.4 问题四:电池续航达不到预期
现象描述:标称450mAh锂电池,实测续航只有不到一天。
排查过程:逐模块测电流,结果显示GPS没有真正关闭。我的控制引脚接的是GPS模块的使能脚,但模块手册里写得很模糊,这个脚实际上只能控制内部RF部分,基带部分仍然全速运行。也就是说,表面“关”了,实际还在跑。
解决方案:改用MOS管彻底切断GPS模块供电,待机电流从25mA直接降到了0。真正的低功耗设计不是依赖模块的软件指令,而是从物理上把电源“断掉”,这个思路适用于任何耗电外设。
6. 复刻手环的物料清单与改进方向
如果看到这里你也想动手复刻一个,我整理了一份可以直接照着采购的物料清单,这些基本都是很容易买到的通用模块,不涉及定制PCB,学生党或者个人开发者毫无压力。
6.1 物料清单速查
| 模块 | 型号/规格 | 说明 |
|---|---|---|
| 主控 | ARM Cortex-M4内核开发板 | 64pin或48pin,Flash≥128KB,可直接用经典F103板子 |
| 心率血氧传感器 | 内置算法光学传感器(MAX30102类) | I2C接口,红/红外双LED |
| GPS模块 | ATGM336H(或兼容NEO-6M) | 串口输出NMEA,支持北斗 |
| OLED屏 | 0.96寸,SSD1306,I2C,128x64 | 显示核心信息 |
| Flash芯片 | W25Q64(8MB) | 存储历史数据 |
| 电池 | 3.7V锂电池450mAh(或以下) | 体积受限于外壳 |
| 充电管理 | TP4056 + 锂电池保护板 | 充电电流设500mA |
| 按键 | 轻触开关 x2 | 交互输入 |
| LDO | 3.3V稳压(低压差线性稳压器) | 注意静态功耗 |
| 外壳 | 3D打印定制 / 手环表带 | 保证传感器贴合手腕 |
关于功耗,有一个核心建议:所有外设模块的电源都必须走MOS管开关控制,这样才能真正让MCU在待机时能够关断一切不必要的外设。单独的IO控制引脚可能不够,因为模块的待机电流仍然是毫安级别,与手环的微安级睡眠目标严重不符。
6.2 鸟枪换炮的扩展方向
这个项目的天花板不低,顺着下面几条路子延伸,能解锁更多商业化或竞赛级玩法:
- 加入低功耗蓝牙:用BLE芯片,把心率、血氧、定位数据打包上传手机APP,实现远程健康监护。BLE广播/连接间隔要调成动态功耗模式,让整机平均续航显著延长。
- 增加运动传感器:加一颗三轴加速度计,结合PPG信号做运动伪差消除,运动模式下的心率曲线会更准。基于加速度计还能做计步、睡眠分析、跌倒检测,手环的场景一下就丰富了。
- 升级算法:对PPG数据做FFT频域分析,替代简单阈值峰值检测,心率准确性会更好看。可以把数据定期通过串口上传电脑,用离线工具对比验证算法精度。
- 接入小程序/云平台:把运动记录、位置轨迹、心率血氧数据同步到云端,生成周报与趋势曲线。GPS轨迹地图化展示,是非常加分的演示效果。
我自己在后期的计划里,就打算把BLE加进去,然后做一个小程序端的数据看板。GPIO预留资源也都留满了,整体结构不做大改就能直接扩展。
6.3 给新手的三点最实在的建议
第一,先模块化验证,再整机集成。每个传感器单独写驱动、单独测试数据格式,确认没问题后,再逐步合并到一套主循环里。我见过太多同学上来就把全部代码压在一起,结果处处是bug,完全无法定位问题在哪。
第二,重视功耗,哪怕初期先不管。项目做完后,用万能表把每一路电源都测一遍,把电流数据记录下来,你会发现优化空间特别大。功耗问题直接影响手环的真实体验,续航两小时和两天是质变。
第三,外壳设计尽早启动。3D打印的外壳做回来也要两三天,如果发现传感器贴合度不行还得返工。所以,硬件验证通过后,马上把外壳设计提上日程,不要等最后才匆忙处理。
7. 我对这个项目的复盘体会
整个项目从规划到稳定运行,我把绝大多数业余时间都填了进来。最难的部分不是传感器驱动,也不是GPS解析,而是让所有外设一致地稳定运行——心率数据要连续可信,定位要随时可靠,界面要流畅刷新,功耗还得压得住。做到最后所有模块都能安稳协同的时候,才真正体会到“系统集成”这四个字的含金量。
个人最受益的一个设计决定,是给外设都加了物理电源控制。后来测试中发现,不需要时把不工作模块的电源彻底切断,不仅电流省下来了,模块间的串扰问题也减少了大半。信号调理电路、I2C总线冲突、电源噪声,这些难点只要电源管理做清爽,就自动消失了。
如果你也想尝试一个综合性强的嵌入式项目,这款心率血氧GPS手环是个很值得投入的选择。不一定要照着完全复刻,拿来练手也值。如果只在代码层面玩过传感器单品,这个项目会让你的调试经验和系统思维都有一次实实在在的跃升。
最后分享一个小技巧:所有串口调试信息,统一用带时间戳的格式输出,排查问题时能在时间轴上精确定位哪条数据异常。当初现场数据乱成一团时,这个习惯直接帮我省掉了大半的定位时间。