健康监测设备这几年从医院场景快速往个人和家庭场景渗透,心率、血氧、体温这些基础生理参数的连续采集,已经不再是专业医疗设备的专属能力。我接触过不少做嵌入式方向的朋友,尤其是学生和刚入行的工程师,第一个真正意义上的完整项目往往就是这类便携式健康监测系统。原因很直接:它同时覆盖了传感器驱动、模拟信号采集、数字通信、人机交互、电源管理这几个嵌入式核心技能点,做完一遍基本能把STM32的常用外设摸个遍。这篇内容围绕一个基于STM32的便携式健康监测系统展开,把硬件选型、原理图设计思路、程序架构、元件清单整理这几个环节拆开讲清楚,适合正在做课程设计、毕业设计或者想练手完整嵌入式项目的读者参考。整套方案的核心思路是:用STM32F103系列做主控,搭配心率、血氧、体温三类传感器,通过OLED本地显示,同时预留串口或无线模块接口用于数据上传,整机用锂电池供电并做充电管理。
1. 为什么这个项目值得完整做一遍
1.1 它到底覆盖了哪些嵌入式硬技能
很多人做STM32项目容易陷入一个误区:拿现成的开发板,插上传感器模块,跑通例程,就算完成了。但便携式健康监测系统如果只做到这一步,其实什么都没学到。这个项目的价值在于它逼着你把一整套链路走通。
从信号层面看,心率采集涉及光电容积脉搏波(PPG)的模拟前端处理,血氧需要双波长LED驱动和光电二极管接收,体温则是数字传感器通过单总线或I2C通信。这三类传感器恰好代表了嵌入式里最常见的三种信号形态:模拟小信号、需要时序驱动的光电信号、标准数字总线信号。把这三种都处理一遍,你对ADC采样、定时器PWM、GPIO时序模拟、I2C/单总线协议的理解会上一个台阶。
从系统层面看,便携设备意味着电池供电,这就引入了电源管理的问题。锂电池充放电保护、升压或降压稳压、低功耗模式切换,这些在插电开发板上永远遇不到的问题,在这个项目里全部冒出来。我见过太多人做完项目后连自己板子的功耗都没测过,这在便携设备里是不可接受的。
1.2 便携式这三个字带来的真实约束
“便携式”不是随便加的前缀,它直接决定了设计取舍。第一是体积约束,传感器和主控要尽量选集成度高、封装小的型号;第二是功耗约束,不能像桌面设备那样一直全速运行;第三是供电约束,锂电池电压范围通常在3.0V到4.2V之间,而STM32和多数传感器需要3.3V稳定供电,这中间必须有稳压环节。
提示:很多初学者直接用开发板上的5V USB供电做原型,功能跑通了就以为大功告成,结果一换成锂电池就各种异常。根本原因是没考虑电池电压跌落时稳压芯片的压差问题。选LDO时要特别关注dropout电压参数。
1.3 适合什么基础的人上手
这个项目对基础的要求其实不算高,但也不是零基础能直接啃的。我的建议是:你至少要用STM32点过灯、用过串口打印、配置过定时器中断。如果这些还没做过,先花一周把GPIO、USART、TIM这三个外设的例程跑一遍再回来。传感器驱动部分,只要理解I2C和单总线的时序概念就行,具体协议可以边做边查手册。
整套方案的工作量,如果每天投入三四个小时,从画原理图到程序调通,大概需要两到三周。其中花时间最多的往往不是写代码,而是调试传感器信号和排查硬件连接问题。
2. 主控与传感器选型背后的取舍逻辑
2.1 为什么是STM32F103C8T6而不是别的
主控选型上,STM32F103C8T6几乎是这类项目的默认答案,但它为什么合适,值得说清楚。这颗芯片是Cortex-M3内核,主频72MHz,64KB Flash,20KB SRAM,LQFP48封装。对于健康监测系统来说,运算量并不大,72MHz绰绰有余,关键是它的外设资源刚好够用:多路12位ADC用于模拟信号采集,多个定时器用于PWM和输入捕获,硬件I2C和USART都有,GPIO数量也够接显示屏和按键。
对比几个常见替代方案:如果用C51单片机,ADC精度和速度都不够,而且没有硬件I2C,驱动OLED会很吃力;如果用STM32F4系列,性能过剩,成本翻倍,对便携设备来说不划算;如果用ESP32,虽然自带无线,但模拟采集部分的稳定性和生态资料丰富度不如F103。所以F103C8T6是在性能够用、成本可控、资料充足这三个维度上的平衡点。
| 主控型号 | 内核 | 主频 | Flash | 价格区间 | 适用性 |
|---|---|---|---|---|---|
| STM32F103C8T6 | Cortex-M3 | 72MHz | 64KB | 低 | 最合适 |
| STC89C52RC | 8051 | 12MHz | 8KB | 极低 | ADC不足 |
| STM32F407 | Cortex-M4 | 168MHz | 512KB | 高 | 性能过剩 |
| ESP32 | Xtensa | 240MHz | 4MB | 中 | 模拟偏弱 |
2.2 心率与血氧传感器的方案对比
心率和血氧这两个参数,市面上有两条技术路线。一条是分开用两个传感器,比如用Pulse Sensor做心率,用MAX30102做血氧;另一条是用MAX30102这类集成芯片同时测心率和血氧。我强烈建议走集成路线。
MAX30102内部集成了红光LED、红外LED、光电二极管和低噪声模拟前端,通过I2C输出原始数据,心率算法和血氧算法在MCU端处理。它的优势是体积小、集成度高、功耗可控,而且两个参数共用一套光路,省去了额外的传感器布局。Pulse Sensor虽然便宜,但它是模拟输出,需要占用ADC通道,而且抗干扰能力差,稍微动一下波形就乱了。
注意:MAX30102的原始数据不是直接的心率和血氧值,需要做滤波和峰值检测才能算出心率,血氧则需要根据红光和红外光的比值R来计算。这部分算法是项目的难点,后面会专门讲。
2.3 体温采集为什么选数字传感器
体温采集我推荐用DS18B20或者类似的数字温度传感器,而不是热敏电阻加ADC的方案。原因有三点:第一,DS18B20是单总线数字输出,精度可达0.5摄氏度,不需要额外的ADC和运放电路;第二,它出厂校准过,省去了热敏电阻需要的标定环节;第三,单总线只需要一个GPIO,布线简单。
热敏电阻方案虽然成本更低,但需要分压电路、ADC采样、查表或公式换算,而且个体差异大,每颗都要标定,对便携设备来说得不偿失。如果对精度要求更高,可以考虑MAX30205这类I2C接口的体温传感器,精度能到0.1摄氏度,但价格贵不少。
2.4 显示与交互部件的选择
显示部分用0.96寸OLED是最常见的选择,SSD1306驱动,I2C或SPI接口。I2C版本接线少,两根线搞定,适合便携设备。分辨率128x64,显示心率、血氧、体温三个数值加上单位完全够用。如果想让界面好看一点,可以加一些简单的图标和进度条。
交互方面,两到三个按键就够了:一个用于切换显示页面,一个用于开关机或进入设置,一个备用。按键用普通轻触开关接GPIO,配合内部上拉或外部上拉电阻,软件做消抖处理。不需要用触摸屏,那会增加成本和功耗,对这个小系统来说是过度设计。
3. 原理图设计中的关键电路拆解
3.1 最小系统电路不能照抄开发板
STM32最小系统包括晶振电路、复位电路、启动模式配置、电源滤波。这几部分看起来简单,但便携设备上有几个细节和开发板不一样。
晶振部分,外部用8MHz无源晶振配合两个20pF左右的负载电容,经过内部PLL倍频到72MHz。负载电容的具体值要根据晶振规格书来定,不是固定20pF,我见过有人照抄开发板用22pF,结果晶振起振困难。复位电路用10K上拉电阻加100nF电容,这个基本通用。启动模式BOOT0和BOOT1通过电阻下拉到地,正常从Flash启动。
电源滤波是重点。每个VDD引脚旁边都要放一个100nF陶瓷电容,靠近引脚放置。VDDA(模拟电源)要额外加一个1uF到10uF的电容,并且通过磁珠或0欧电阻和数字电源隔离。这一点在健康监测系统里特别重要,因为ADC采集的是微弱生理信号,数字部分的开关噪声会直接串到模拟电源上,导致采样值跳动。
3.2 锂电池充电与稳压电路
便携设备用单节锂电池供电,标称电压3.7V,充满4.2V,放空约3.0V。充电管理用TP4056这类线性充电芯片,它支持最大1A充电电流,通过一个电阻设定充电电流,带充电状态指示。TP4056的输入是5V USB,输出接电池正极。
稳压部分,因为STM32和传感器需要3.3V,而电池电压在3.0V到4.2V之间波动,所以不能用普通LDO直接降压,因为当电池电压降到3.3V以下时LDO就无法维持输出。解决方案有两种:一是用低压差LDO,dropout电压要低于0.3V;二是用升降压芯片,比如TPS63020这类,电池电压高于3.3V时降压,低于3.3V时升压。我建议用升降压方案,虽然成本高一点,但电池利用率高,不会出现电压一低就重启的问题。
| 电源方案 | 芯片示例 | 优点 | 缺点 |
|---|---|---|---|
| 普通LDO | AMS1117 | 便宜简单 | 电池低于3.5V失效 |
| 低压差LDO | XC6206 | 压差小 | 低于3.3V仍失效 |
| 升降压 | TPS63020 | 全电压范围稳定 | 成本较高 |
3.3 传感器接口电路的细节
MAX30102的接口电路需要注意几点。它的INT引脚可以配置为中断输出,当FIFO有数据时触发,这样MCU就不用一直轮询,可以降低功耗。SCL和SDA需要上拉电阻,典型值4.7K,但如果总线上挂的设备多或者走线长,要适当减小。LED的供电引脚要单独加滤波电容,因为LED驱动瞬间电流较大,会在电源上产生纹波。
DS18B20的DQ引脚需要4.7K上拉电阻,这是单总线协议的要求。如果走线较长,可以考虑减小到2.2K,但不要太小,否则总线电容充放电时间不够。传感器供电用3.3V,注意DS18B20有寄生供电模式,但我不推荐用,因为寄生供电在温度转换期间需要强上拉,电路更复杂,不如直接三线供电省心。
OLED的I2C接口和MAX30102可以共用一条总线,只要地址不冲突。SSD1306的地址通常是0x3C或0x3D,MAX30102是0x57,不冲突。共用总线时上拉电阻只需要一组,放在总线靠近MCU的一端。
3.4 调试接口与预留扩展
SWD调试接口一定要留出来,四根线:VCC、GND、SWDIO、SWCLK。便携设备上可以用1.27mm间距的排针,节省空间。串口TX/RX也建议引出,方便打印调试信息和后续接无线模块。
扩展方面,可以预留一个UART接口给蓝牙模块或WiFi模块,用于把数据传到手机或云端。如果只是本地监测,这部分可以不焊。另外可以留一个Micro USB接口,既用于充电,也用于串口通信,通过CH340这类USB转串口芯片实现。
4. 程序架构与传感器驱动的实现要点
4.1 整体软件框架怎么搭
程序不建议写成一个大循环里塞所有逻辑,那样后期改起来很痛苦。我推荐用前后台架构:主循环负责显示刷新、按键扫描、数据处理,定时器中断负责周期性的传感器采样触发。如果对实时性要求更高,可以上FreeRTOS,但对这个项目来说有点重,前后台足够。
具体分工是这样的:TIM2配置为1ms中断,用于系统时基和按键消抖计时;TIM3配置为PWM输出,用于驱动MAX30102的LED(如果不用芯片内部驱动的话);USART1用于调试打印;I2C1用于OLED和MAX30102;GPIO用于DS18B20和按键。主循环里每100ms读一次传感器数据,每200ms刷新一次OLED。
4.2 MAX30102的初始化和数据读取
MAX30102的初始化流程是:复位、配置FIFO、设置LED电流、设置采样率和ADC量程、使能中断。关键寄存器包括MODE_CONFIG(设置工作模式,心率模式或血氧模式)、SPO2_CONFIG(设置采样率和LED脉宽)、LED1_PA和LED2_PA(设置红光和红外LED电流)。
数据读取有两种方式:轮询和中断。轮询就是定期读FIFO的写指针和读指针,差值就是可用样本数。中断方式是把INT引脚接到MCU的外部中断,FIFO有数据时触发中断,在中断里读数据。我建议用中断方式,功耗更低。
// MAX30102读取FIFO数据的简化示例 void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) { uint8_t buffer[6]; I2C_ReadBytes(MAX30102_ADDR, FIFO_DATA_REG, buffer, 6); *red = ((uint32_t)buffer[0] << 16) | ((uint32_t)buffer[1] << 8) | buffer[2]; *red &= 0x03FFFF; // 18位有效 *ir = ((uint32_t)buffer[3] << 16) | ((uint32_t)buffer[4] << 8) | buffer[5]; *ir &= 0x03FFFF; }4.3 心率计算:从原始波形到BPM
拿到红光和红外原始数据后,不能直接当心率用。原始信号里包含直流分量(反映组织、静脉血等静态吸收)和交流分量(反映动脉搏动)。心率信息在交流分量里,所以第一步是去直流,可以用滑动平均或者一阶高通滤波。
去直流后得到脉搏波波形,接下来做峰值检测。简单做法是设定一个动态阈值,当信号从下方穿越阈值向上时记录一个峰值,两个峰值之间的时间间隔就是一次心跳周期。用60除以周期(秒)就得到BPM。但实际信号有噪声,需要加一些约束:比如相邻峰值间隔不能小于300ms(对应200BPM),不能大于2000ms(对应30BPM),超出范围的就丢弃。
// 简化的峰值检测逻辑 #define MIN_PEAK_INTERVAL 300 #define MAX_PEAK_INTERVAL 2000 static uint32_t last_peak_time = 0; static uint8_t peak_count = 0; static float bpm_buffer[4] = {0}; static uint8_t bpm_index = 0; void ProcessHeartRate(uint32_t ir_value, uint32_t timestamp) { static uint32_t dc_filter = 0; static int32_t last_value = 0; int32_t ac_value; // 一阶高通去直流 dc_filter = dc_filter + ((ir_value - dc_filter) >> 6); ac_value = (int32_t)ir_value - (int32_t)dc_filter; // 上升沿穿越阈值检测 if (last_value < 0 && ac_value >= 0) { uint32_t interval = timestamp - last_peak_time; if (interval > MIN_PEAK_INTERVAL && interval < MAX_PEAK_INTERVAL) { float bpm = 60000.0f / interval; bpm_buffer[bpm_index++ % 4] = bpm; // 四点平均 float sum = 0; for (int i = 0; i < 4; i++) sum += bpm_buffer[i]; current_bpm = sum / 4.0f; } last_peak_time = timestamp; } last_value = ac_value; }4.4 血氧饱和度的比值计算法
血氧的计算原理是基于红光和红外光在含氧血红蛋白和脱氧血红蛋白中吸收率不同。定义R值为:(红光交流分量/红光直流分量) / (红外交流分量/红外直流分量)。然后通过经验公式SpO2 = A - B*R 换算,A和B是校准系数,典型值A约110,B约25,但具体值需要根据传感器和测量部位标定。
实际实现时,分别对红光和红外通道做去直流,提取交流分量的峰峰值或有效值,然后算R值。这里要注意,红光和红外的采样是同步的,所以交流分量的提取要对齐时间窗口。血氧的精度比心率更难做,因为受运动伪影和佩戴松紧影响很大,建议在静止状态下测量,并且做多次平均。
4.5 DS18B20的单总线时序实现
DS18B20用单总线协议,所有通信靠一根线完成,对时序要求严格。STM32上可以用GPIO模拟,也可以用UART配合单总线模式,但模拟更通用。关键时序包括:复位脉冲(拉低至少480us)、存在脉冲检测(释放后等待15到60us,传感器拉低60到240us)、写0(拉低60us后释放)、写1(拉低1到15us后释放)、读时隙(拉低1us后释放,在15us内采样)。
// DS18B20复位和存在检测 uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_DQ_OUT(); DS18B20_DQ_LOW(); delay_us(480); DS18B20_DQ_HIGH(); DS18B20_DQ_IN(); delay_us(60); presence = DS18B20_DQ_READ(); // 0表示存在 delay_us(420); return presence; }温度转换需要时间,12位精度下最大750ms。可以发转换命令后先去做别的事,过一会儿再读。读回来的16位数据,低4位是小数部分,高12位是整数部分,乘以0.0625就是摄氏度。
5. 元件清单整理与采购避坑
5.1 核心元件清单
| 类别 | 型号 | 数量 | 说明 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 1 | LQFP48封装 |
| 心率血氧 | MAX30102 | 1 | 集成模块或芯片 |
| 体温 | DS18B20 | 1 | 防水封装可选 |
| 显示 | 0.96寸OLED | 1 | SSD1306,I2C接口 |
| 充电管理 | TP4056 | 1 | 带保护版本 |
| 稳压 | TPS63020 | 1 | 升降压 |
| 电池 | 锂电池 | 1 | 3.7V,500到1000mAh |
| 晶振 | 8MHz | 1 | 无源 |
| 按键 | 轻触开关 | 3 | 6x6mm |
| 电阻电容 | 若干 | - | 0402或0603封装 |
5.2 采购时容易踩的坑
MAX30102市面上有芯片和模块两种。模块已经集成了上拉电阻和滤波电容,接线方便,但体积大;芯片便宜但需要自己画外围电路。如果是做便携设备,建议用芯片自己画,模块适合快速验证。
锂电池要注意带保护板,否则过放会损坏。TP4056要选带DW01保护芯片的版本,这样过充过放都有保护。OLED买的时候注意是I2C还是SPI版本,别买错了。DS18B20有原装和国产之分,国产的精度和一致性差一些,如果对温度精度要求高,建议买原装。
提示:PCB打样前一定要把原理图仔细检查一遍,特别是电源部分的正负极和芯片的引脚定义。我见过有人把TP4056的BAT+和BAT-画反,上电直接冒烟。
5.3 PCB布局的几点经验
便携设备的PCB布局,电源部分和模拟部分要分开。TP4056和稳压芯片放在板子一侧,MAX30102和DS18B20的走线尽量短,远离晶振和开关电源。OLED的排线如果长,要加屏蔽或者双绞。
地平面尽量完整,模拟地和数字地单点连接。MAX30102下面的地要干净,不要走数字信号线。晶振尽量靠近MCU,负载电容紧贴晶振引脚。SWD接口放在板边,方便插调试器。
6. 调试过程中最容易卡住的几个问题
6.1 心率数据一直跳动怎么办
这是最常见的问题。首先要确认传感器佩戴是否贴合,MAX30102要贴在皮肤上,不能有漏光。然后检查LED电流设置,太小信号弱,太大功耗高且可能饱和。一般红光和红外LED电流设在6到10mA之间比较合适。
软件上,去直流的滤波系数很关键。系数太小,直流跟踪慢,波形基线漂移;系数太大,交流分量被削弱。我一般用右移6位,相当于时间常数64个采样点,在50Hz采样率下约1.3秒,实测比较合适。如果还是跳,可以加一个中值滤波,把明显异常的BPM值剔除。
6.2 OLED不亮或者花屏
先查I2C地址对不对,用逻辑分析仪或者示波器看SCL和SDA有没有波形。如果地址对但没显示,检查初始化序列是否完整,SSD1306需要一系列命令配置。花屏通常是I2C速率太高或者上拉电阻不合适,把速率降到100kHz试试,上拉电阻换成4.7K。
还有一种情况是电源问题,OLED在3.3V下工作,但如果稳压输出不稳,显示会异常。用万用表量一下OLED的VCC引脚,正常应该在3.3V左右。
6.3 锂电池供电时系统反复重启
这个问题的根源通常是稳压芯片的压差或者电流能力不够。当MAX30102的LED点亮瞬间,电流会突然增大,如果稳压芯片响应慢或者输出电容不够,电压会瞬间跌落导致MCU复位。解决办法是在稳压输出端加大电容,至少100uF,并且选响应快的芯片。
另外检查电池保护板的内阻,劣质保护板内阻大,大电流时压降明显。可以用示波器抓一下电池电压和3.3V输出的波形,看复位瞬间有没有跌落。
6.4 DS18B20读出来一直是85度
85度是DS18B20的上电默认值,说明温度转换没有成功或者读取时序有问题。先确认复位和存在脉冲是否正常,如果存在脉冲检测不到,说明接线或上拉电阻有问题。如果存在脉冲正常但读出来是85,检查转换命令是否发出,以及等待时间是否足够。12位精度需要750ms,如果提前读就会得到旧值或默认值。
还有一种可能是供电不足,DS18B20在转换期间电流会增大,如果电源带不动,转换会失败。用示波器看DQ线上的波形,正常应该有完整的复位脉冲和读写时隙。
7. 从能跑到好用还差哪些优化
7.1 低功耗设计的实际做法
便携设备如果一直全速运行,电池撑不了多久。低功耗的核心思路是让MCU和传感器在不需要工作时进入睡眠。STM32F103有Sleep、Stop、Standby三种低功耗模式,Stop模式下功耗可以降到几十微安。
具体做法是:主循环里如果没有按键事件,就让MCU进入Stop模式,用RTC或者外部中断唤醒。MAX30102可以配置为每秒钟只采样几次,而不是连续采样。OLED在无操作一段时间后关闭显示。这样整机平均电流可以控制在几毫安,500mAh电池能用好几天。
7.2 数据存储与上传的扩展
如果想把数据存下来,可以加一个SPI Flash或者TF卡。TF卡通过SPI接口,用FatFS文件系统,把心率、血氧、体温按时间戳存成CSV文件。这样后续可以用Excel或者Python做趋势分析。
上传方面,预留的UART接口可以接HC-05蓝牙模块或者ESP-01S WiFi模块。蓝牙模块配置成串口透传,手机端用蓝牙串口助手就能收到数据。WiFi模块可以连到本地服务器或者云平台,但要注意功耗会增加不少。
7.3 外壳与佩戴方式的考虑
做便携设备,外壳不能忽略。3D打印一个简单的外壳,把PCB、电池、传感器固定好。传感器要露出外壳,方便贴合皮肤。手腕式的话,可以用表带固定;指夹式的话,结构要设计成能夹住手指。
佩戴方式直接影响信号质量。手腕式测量时,传感器要压在桡动脉上方;指夹式要保证手指不透光。我试过用黑色热缩管包裹传感器周围,减少环境光干扰,效果很明显。
8. 写在最后的一点个人体会
这个项目我从画原理图到调通花了大概三周,其中有一周时间卡在血氧算法上。后来发现是红光和红外的直流分量提取窗口没对齐,导致R值算出来偏差很大。把两个通道的滤波参数统一之后,血氧值就稳定了。所以如果你也在做类似的东西,遇到数据不对,先别急着改算法,回头检查一下信号采集的同步性和滤波参数的一致性,往往问题就出在这些基础环节。
另外,元件清单里的东西不用一次买齐,可以先买核心的几样,把最小系统跑起来,再逐步加传感器。这样即使某个模块有问题,也不会影响整体进度。PCB打样建议先打一版验证,确认没问题再批量做,毕竟改板的时间和成本都比改代码高得多。