简介:本资源是一套完整的基于STM32的多功能健康监测系统毕业设计/课程设计实现方案,面向嵌入式初学者、单片机课程实践者及本科毕设学生,解决便携式生理参数实时采集、处理与无线传输的核心工程问题。压缩包共356个文件,75.11MB,涵盖70余个C源文件与77个头文件(构成STM32底层驱动与算法逻辑)、15份PDF文档(含硬件原理图、PCB设计图及蓝牙健康管理技术说明)、3个APK安装包(配套安卓端蓝牙数据显示应用)、以及Keil工程文件(uvprojx/uvoptx)、编译中间文件(o/obj/hex)和调试配置(dbgconf),目录结构完整体现“硬件设计—固件开发—移动端交互”全链路。目前已有36人学习下载,用户可直接复现心率、体温、血氧、血压等多参数采集流程,获取可运行的Keil工程、已验证的蓝牙通信协议栈、PCB制板文件及移动端可视化方案,具备强实操性与教学参考价值。
1. 拿到“多功能健康监测系统”压缩包后,我为什么先画了三个小时框图
先交代一下背景。这个项目是一个典型的STM32实战题目:基于STM32的多功能健康监测系统设计。很多人第一次拿到这类压缩包,第一反应是解压、打开原理图、看代码能不能编译过,然后烧录跑个Demo,屏幕亮了就以为完工。我的习惯恰恰相反——拿到需求先花两三个小时把系统框图和数据流理清楚,再动手写代码。这个习惯帮我避开了至少一半的返工。
先说这个系统整体要做什么。健康监测的“多功能”,在实际项目中通常落在四个维度上:
| 功能维度 | 常见实施方案 | 核心芯片/模块 | 数据特点 |
|---|---|---|---|
| 心率/血氧 | 光电容积脉搏波 | MAX30102 | I2C输出,需滤波算法 |
| 体温测量 | 数字温度传感器 | DS18B20 / NTC | 响应慢,需标定 |
| 环境温湿度 | 数字温湿度计 | DHT11 / AHT20 | 单总线/I2C,时序敏感 |
| 数据显示/上报 | OLED + WiFi | SSD1306 + ESP8266 | 数据量大,刷新限速 |
如果再加上电池供电,就还要考虑ADC采集电池电压、低功耗模式切换、历史数据存储到Flash、通过ESP8266上报到云平台或手机。整个系统从传感器到最终展示,是一条完整的数据通路。
我推荐的主控是STM32F103C8T6,也就是那款经典的蓝色Pill开发板核心芯片。选它的理由很实际:便宜(x宝几块钱一片)、资料多、HAL库和标准库都有大量现成例程,踩坑时一搜一大把解决方案。关键是这款芯片有64KB Flash和20KB RAM,跑一个健康监测的裸机程序绰绰有余,不需要上FreeRTOS,当然想上也可以,后面我会讲怎么为每个任务分配优先级。
数据流上,我的设计是这样:传感器通过I2C、单总线、ADC三条通路把原始数据送进STM32,在MCU内部做数据处理(滤波、标定、异常判断),然后分三路输出——一路去OLED做本地显示,一路写Flash做历史记录,一路走串口/ESP8266做远程上报。电池电压通过ADC采集,作为第四路输入,影响系统的低功耗策略。
这个架构看起来简单,但真正写代码时你会发现,每个传感器都有自己的时序脾气,MCU资源在交互和采样之间争夺,Flash写入擦除会卡顿主流程,ESP8266的AT指令会拖慢整个系统。这些问题不在一开始想清楚,后面就是无休止的补丁。
所以这篇文章,我会从数据采集链路、人机交互与存储、实测误差分析、踩坑记录、扩展方向五个方面,把这个项目的完整实现路径和背后的原因讲透。内容偏工程实践,适合正在做课程设计、毕业设计,或者想拿STM32做小型医疗电子项目的开发者参考。
2. 传感器数据采集链路的搭建与校准:每个模组都不是插上就能用
2.1 心率血氧模块MAX30102:I2C驱动只是开始
MAX30102是这颗系统里最容易“假装成功”的传感器。它在模块上已经集成了红光LED、红外LED和光电二极管,MCU只需要通过I2C读寄存器就能拿到红光和红外的ADC值。I2C地址是0x57,这没问题,但如果你以为读出来就是心率值,那就误会大了。
原始ADC值是一个脉动波形,你需要做几件事:
第一,配置采样率和LED电流。MAX30102的采样率可以设置到50Hz到3200Hz。心率监测不需要太高,100Hz足够,但血氧的PPG(光电容积脉搏波)分析需要稳定的AC和DC分量。LED电流我习惯设置到6.4mA左右,太小信号幅度不够,太大容易饱和。
第二,做数字滤波。脉搏波的频率范围大概在0.5Hz到5Hz之间(对应30到300次/分钟的心率),所以要用带通滤波把高频噪声和基线漂移去掉。裸机程序里我用了简单的滑动平均加一阶高通滤波的组合,效果够用。这里有个经验:不要一开始就上自适应滤波或者小波变换,先画出波形看看大致形态,再决定用什么算法。
第三,计算心率和血氧。心率可以用时域波峰检测,也就是找相邻两个脉搏波峰的时间间隔,60除以间隔就是每分钟心率。血氧的计算公式是:
SpO2 = 110 - 25 * R其中R = (AC_red / DC_red) / (AC_ir / DC_ir),也就是红光和交流分量比除以红外的交流分量比,实际工程里需要通过标定拟合系数,不能直接套理论公式。很多模块出厂时的默认校准对大多数人是够用的,但因为个体差异(皮肤颜色、手指厚度、佩戴松紧),你会看到血氧数值有±2%左右的浮动,这是正常现象。
这里我要特别强调一个坑:MAX30102极其容易受到环境光干扰。我调试时第一次把传感器对着窗台自然光,波形完全没法看,还以为是模块坏了。后来用黑色热缩管把传感器侧面和顶部遮光,只留一个手指接触窗口,波形立刻清晰了。做健康监测产品的朋友,遮光结构一定要考虑进去。
2.2 体温测量:DS18B20还是NTC,其实是个使用场景问题
体温是这个系统里最容易被低估的传感器。很多人直接买DS18B20,因为驱动简单(单总线协议),精度号称0.5℃,勉强够用。但实际测试下来,DS18B20的响应速度非常慢,从接触皮肤到读数稳定,可能需要30秒以上,而且读数会受到环境温度的明显影响。
我的方案是DS18B20做环境温度参考,体温探头用NTC热敏电阻加一个分压电路接到ADC。NTC的响应速度取决于热容量,贴在皮肤上几秒钟就能稳定,配合一个简单的低通滤波变化很平滑。
NTC的读数是查表或公式计算,我用的是B值公式:
R = R25 * exp(B * (1/T - 1/T25))其中R25是25℃时的标称阻值(常用10kΩ),B值一般是3435或3950,T为绝对温度。实际工程里,这个公式的精度受限于B值的离散性,尤其是温度范围跨越大时误差会增大。如果只需要显示一位小数,这个精度够用;如果想要精准测量,建议用已知温度源做两点标定,反推实际B值。
有一点要提醒:体温监测和人体的接触方式非常影响结果。腋下测量需要夹紧,额头测量需要红外传感器而非接触式,耳温又需要专门的探头。做毕设或课程设计时,建议明确标注“接触式皮肤表面温度”,而不是“体温”,避免误导。我自己的项目里把采集结果显示为“体表温度”,跟真正的医学体温测量分开。
2.3 环境温湿度DHT11:便宜,但要伺候好它的时序
DHT11这个传感器被无数人吐槽过,但它依然出现在大量项目里,原因就是便宜、够用、好接线。可是DHT11有个非常折磨人的特性:它上电后的第一次读取大概率是失败的,而且两次读取间隔必须大于1秒,否则它根本不响应。
所以DHT11的驱动,写代码时一定要有三要素:上电后延时至少1秒再读;每次读完等1.5秒以上再读下一次;读取过程中要关中断,避免MCU被其他中断打扰而错过时序窗口。
如果用AHT20替代DHT11,体验会好很多——I2C接口、内部自带校准、读取间隔只需要几百毫秒,精度更高,价格也只贵一两块钱。如果做的是商业产品原型,建议直接上AHT20。但如果你的项目要求必须用指定的DHT11,那就老老实实把时序做好,特别注意:
// 读取DHT11的一段伪代码示意 GPIO_Set_Mode(IN_PULL_UP); delay_ms(20); // 拉低至少18ms触发 GPIO_Reset; delay_ms(20); GPIO_Set_Mode(IN); // 等待响应,注意超时保护这里最关键的技巧是超时保护。如果DHT11没响应,程序会卡在读时序的循环里,整个系统看起来像死机了一样。我在每个等待循环里都加了计数器和超时跳出,读不到数据就返回错误码,主循环还能继续跑。这个思路放在任何传感器驱动里都适用。
2.4 ADC多通道+DMA:电池电压采集里的数据错位雷区
这类系统如果要电池供电,必须采集电池电压,用来判断低电量。STM32F103的ADC是12位的,我把它配置成扫描模式,多通道循环采样,通过DMA把结果搬进内存,避免CPU逐次查询浪费事件。
多通道扫描模式的一个常见问题是数据错位。如果你用ADC的扫描模式连续采集多个通道,DMA的缓冲区顺序必须和ADC的通道顺序严格一致。我先启动一次DMA传输再触发转换,每次采样完再重新启动,保证数据始终对应正确的通道。否则你会看到电压值偶尔跳到另一个通道的数据上,排查起来相当费劲。
DMA缓冲区我用了双缓冲思想(但不是DMA的双缓冲机制,而是自己在主循环里交替使用两块缓冲区)。实际用裸机做,一个简单的方法是:DMA中断里标记“数据已就绪”,主循环只读取“当前有效缓冲区”,读取完再切换。这样ADC在背后不断采样,主循环不会因为读数据而阻塞。
电池电压的分压电阻也要选对。锂电池满电4.2V,直接进ADC会超出3.3V参考电压,所以要用两个电阻分压,把最高电压映射到3.3V以内。我用的是100kΩ和47kΩ的分压,理论分压比是100/(100+47)=0.68,4.2V分压后约2.86V,安全。分压电阻精度最好选1%的,否则测出来的电压会有不可忽视的偏差。
3. 数据怎么显示、怎么存、怎么上传:从菜单到云端的设计思路
3.1 OLED屏上的医用级显示设计
我选了0.96寸的I2C接口OLED(SSD1306),128x64分辨率。驱动代码到处都有,关键是UI和刷新逻辑。健康监测不是日志打印,界面要让人一眼看明白,我做了一个三屏循环结构:
- 主屏:显示当前心率、血氧、体表温度三个核心指标,大号字体显示心率值,方便老人看;
- 第二屏:显示环境温湿度、电池电压和通信状态;
- 第三屏:显示历史数据的简单趋势图,或者最近24小时的最高/最低值。
用字库的时候,SSD1306内置的ASCII字库简单,但中文显示要外挂取模字库。考虑到“心率”“血氧”“体温”这几个中文词是固定文本,我直接做了几个16x16的中文点阵,这样节省了很多Flash空间,不用上全字库。
刷新策略也很重要。OLED全屏刷新一次大概要几十毫秒,如果每帧都全刷新,会有明显的闪烁感。我的方案是:静态显示的内容只在数据变化时重绘对应区域,用SSD1306设置显示窗口的方式只更新数值区域。这部分用I2C向OLED写入数据时,对I2C总线的占用率也要控制,否则会影响MAX30102的I2C通信。I2C本身就是共享总线,两个从设备都能工作,但频繁的大数据量写入会抢总线时间片,我实测下来OLED刷新时心率读数偶尔会出现丢帧,所以在显示和采集之间做了简单的分时:采样完成后才允许OLED刷新,采样过程里如果有数据要显示,先缓存在本地变量里。
3.2 历史数据Flash存储:不让擦写卡死整个系统
STM32F103C8T6内置64KB Flash,程序占用约20KB,剩下的空间可以存历史数据。每一条记录(心率、血氧、体温、时间戳)我用8个字节存储,大约能存5000条,按每分钟存一条算,能存3天半。对原型来说够用了。
直接调用HAL库的Flash写入函数会有个问题:写Flash前必须先擦除扇区,而擦除期间Flash是忙的,如果主循环正在擦除,整个系统会卡顿几百毫秒到一秒钟。放在健康监测这种需要稳定采样的场景里,这种卡顿是致命伤。
处理思路是这样:
- 开辟一个RAM缓冲区,数据先写进RAM,缓冲区满(比如写满一页)再一次性写入Flash;
- Flash写入前先检查剩余空间,不够就先擦除最老的数据页;
- 擦除操作放在主循环的空闲时间段里,或者用状态机分步执行,避免长时间阻塞;
- 写Flash前关闭所有可能触发中断的外设(特别是串口和定时器),写完后恢复,防止写Flash过程中有中断打断导致Flash操作异常。
实际项目里,我遇到了一个很奇怪的bug:数据写进Flash后重启,读出来前几条正确,后面的全是0xFF。后来才想明白,是STM32对Flash的写入有最小单位限制,我按字节写导致部分写入失败。正确做法是半个字(16位)对齐写入,每次写2字节。这个坑花了我一个晚上。
3.3 ESP8266上报:AT指令JSON拼接和意外掉线
ESP8266在这个系统里承担远程上报功能。我用的是最简单的AT指令方案:STM32通过串口发AT指令控制ESP8266连接WiFi、建立TCP连接、发送数据。有些同学可能觉得AT指令太老,但作为原型演示,它的稳定性和调试便捷性其实最好。
AT指令的核心逻辑:
AT+CWMODE=1 // 设置Station模式 AT+CWJAP="SSID","password" // 连接WiFi AT+CIPSTART="TCP","192.168.1.100",8080 // 建立TCP连接 AT+CIPSEND=45 // 发送45字节数据这里有个细节:AT+CIPSEND后面的字节数必须和实际发送的数据长度严格一致,多一个字符或少一个字符都会导致发送失败。而你要发送的数据是动态拼接的JSON,比如:
{"heart_rate":72,"spo2":98,"temp":36.5,"bat":3.85}长度随数值位数变化。所以每次发送前,先把数据填充进一个字符串缓冲区,计算实际长度,再以%d的形式传给AT指令。
ESP8266和STM32之间是串口,默认波特率我设9600(AT固件默认值),实际调试时经常出现乱码。换成115200后稳定很多,但要注意ESP8266的固件必须支持波特率修改,否则换波特率反而连不上。AT指令响应有长有短,接收中断里我用了环形缓冲区,主循环里做状态机解析,这样即使AT指令返回了“OK”中间又夹着别的数据,也不容易解析错。
掉线问题是另一个高频症状。ESP8266连接的路由器如果开启了AP隔离、MAC过滤或者信号强度不够,MCU这边会一直收不到数据。我的排查顺序是:先用电脑串口调试助手直接给ESP8266发AT指令,确认模块本身能正常连接网络,再检查STM32和ESP8266之间的串口接线是否接反。这个检查顺序省了我很多无效调试时间。
4. 整机联调与实测:仪表盘上的数和人体真实的数差多少
4.1 心率血氧的真实测量对比
联调阶段,我把MAX30102贴到自己手指上,和一台医用级指夹式血氧仪并排比较。结果是这样的:
| 测量项 | 医用指夹仪 | 本系统 | 偏差 |
|---|---|---|---|
| 静态心率 | 72 bpm | 74 bpm | +2 |
| 运动后心率 | 108 bpm | 105 bpm | -3 |
| 血氧静止 | 98% | 97% | -1 |
| 血氧运动 | 96% | 95% | -1 |
心率偏差2-3次属于可接受范围,主要原因是波峰检测的阈值设置不同,运动时波形幅值变化大,可能漏检或多检一个峰。我加了相邻波峰间隔的合理性判断:如果计算出的心率超过220或低于30,大概率是检测异常,直接丢弃这次结果,用上一拍的有效数据。
血氧的偏差来源主要是手指位置不一致。指夹仪是透射式,血流路径固定;MAX30102是反射式,贴在指腹位置不同,光学路径就不同。必须保证模块的位置每次按压力度一致。我在外壳上做了凸起的定位槽,让手指每次放上去都在同一个位置。
4.2 体表温度和环境温度的关系
体表温度测量这块,我的实测数据和日常认知有一个有趣差异。环境温度25℃时,指尖皮肤表面温度大概在30-32℃,腋下是35-36℃。直接用NTC贴指尖测,得到的是“指尖表面温度”,比医学定义的腋下体温低好几度。
为了让结果有意义,我在代码里加了偏移修正:根据环境温度和当前体表温度的差值做线性修正,但修正系数要单独标定。比如说环境25℃时指尖显示温度为31℃,如果要拟合到腋下体温36.5℃,偏移是+5.5℃。但这个偏移在环境温度变化时会漂移,所以修正公式也只能是近似。
需要提醒的是:作为课程设计或电子制作,这个偏移修正逻辑在文档里写清楚“仅作为原理演示,不作为医疗诊断依据”,会大大提升整个项目的信服度。医疗设备有严格的注册标准,公开分享时最好加一个明显的免责提示,这对你也是一种保护。
4.3 功耗实测与低功耗策略
系统用3.7V锂电池供电,裸机没有低功耗优化时,整机电流大约60mA(OLED常亮、MAX30102两路LED点亮、ESP8266持续开启)。这样的功耗,1000mAh的小电池只能撑半天多,对健康监测设备来说不可接受。
我做了三档功耗优化:
运行模式:所有外设正常工作,整机电流60mA,持续1-2分钟完成一次完整测量;
待机模式:OLED关闭、MAX30102关闭、ESP8266进入深度睡眠,仅保留RTC定时唤醒,电流降到约15mA;
休眠模式:MCU进入STOP模式,所有外设关闭,电流约50uA,按键唤醒。
实测下来,如果用户每天主动测量10次,每次1分钟,其余时间待机,一个1000mAh电池能撑10天左右。如果是连续监测模式,那电池容量和充电方案都要重新考虑。
STM32F103的STOP模式唤醒,我用的是外部中断,PA0引脚接按键,按下唤醒后再初始化外设。有个细节:唤醒后系统时钟源要重新初始化,否则外设的波特率会变乱。我就是在唤醒后忘调SystemClock_Config,串口输出全变乱码,排查了半天。
5. 调试过程中踩过的坑:你会遇到但未必能在教程里找到的
5.1 I2C总线上拉电阻缺失导致的“幽灵故障”
PCB或面包板上接MAX30102和OLED时,如果模块上没有自带I2C上拉电阻,总线就会工作不稳定。症状很怪:有时候能读到数据,有时候读不到,用逻辑分析仪看波形,SDA和SCL信号的上升沿很缓,像一个慢坡而不是陡峭的方波。
方案是在两根线上各加一个4.7kΩ上拉电阻到3.3V。如果模块上已经焊了上拉,再加并联电阻会让总线驱动能力不够,反而拖垮波形。做一个统一定律:先查模块原理图,确认是否自带上拉,再决定外接。
还有一个隐患是总线上挂的设备多,每组设备的上拉电阻是并联关系,并联等效电阻太小,I2C总线的低电平可能拉不下去。这时候就表现为I2C通信完全失败。我建议每个I2C总线上只保留一组上拉,其他模块的焊盘上不焊上拉电阻。
5.2 ADC DMA与MAX30102中断的优先级冲突
这个坑发生在一次联调时,现象是OLED显示的心率数值每隔几秒跳变一次,但手指并没有动。我翻看数据,发现每次跳变都出现在ADC的DMA传输完成中断之后。原因清楚了:DMA中断优先级高于MAX30102的数据就绪中断,DMA频繁搬运数据时,MAX30102的中断被长时间挂起,丢失了脉搏波的数据点,导致心率计算值跳变。
解决思路则是把MAX30102中断优先级提到DMA之上,同时在MAX30102的I2C读取里加状态保护,确保数据完整性优先。另外一个办法是把ADC的DMA传输周期拉长,比如每秒才采样一次电池电压,而不是忙轮询。事实证明,大部分情况下减少中断频率比调优先级更根本。
5.3 OLED刷新时I2C总线的抢占总线
前面提到过,OLED每帧刷新要发大量数据。如果OLED刷新期间,STM32同时在跟MAX30102通信,I2C总线就会被OLED长时间占用,心率模块的数据读取自然被拖延。这在单I2C总线上是结构性冲突。
我的处理方案是:把OLED放到慢速刷新循环里(比如每秒刷新5次),并且刷新代码内部检测“当前是否存在MAX30102数据读取请求”,有就暂停OLED刷新一帧。另一个更优方案是OLED用硬件SPI接口,SPI和I2C完全独立,互不抢占。只是SPI OLED需要多接几根线,如果PCB空间允许,建议SPI方案。
5.4 DHT11读不到数据:不是代码问题,是延时被优化了
开发环境里有一种很容易被忽略的坑:Keil的优化选项。我用默认的-O0调试时DHT11一切正常,为了腾出更多Flash空间把优化等级调到-O2后,DHT11突然读不到数据了。原因是编译器把延时函数优化掉了,我写的空循环被识别为无效代码而删除。
解决方法是把延时函数声明为volatile,或者内嵌汇编空操作。最简单的做法是用定时器做延时,彻底摆脱编译器优化影响。从那以后,我在所有对外设时序有严格要求的项目里,一律用定时器或SysTick做延时,不再依赖空循环。像DHT11这种ma级时延的,SYSTick精度也够,注意延时过程中的中断抢占会让实际延时略长,判断时留出20%的裕量。
5.5 Flash写入后系统读到的还是旧数据
这个坑我前面提过,但值得展开讲。STM32的Flash写入,尤其是跨页写入,如果地址没有对齐到2字节(实际是32位寄存器的写入宽度),会写入失败,但HAL库不会报错,只是数据写不进去。读出来的还是0xFF或之前的数据。
我的排查思路是把写Flash和读Flash都封装成专门的函数,在函数入口打印地址、数据长度,重启后验证数据。一旦发现问题,先用官方例程验证Flash驱动本身没问题,再检查自己的地址计算逻辑。实际项目里,有一次是我把Page的概念搞错了,以为一个Page是1KB,实际上是1KB没错,但扇区(Sector)是4KB,擦除必须按扇区来,我擦除时擦小了范围,跨扇区写入时就覆盖了未擦除的区域。这个教训是:一定要细读参考手册里的Flash章节。
6. 进阶方向:这个系统还能往哪里走
整套系统做扎实之后,可扩展的空间其实很大。我这边的思路按投入产出比排序:
第一优先级:用FreeRTOS重构任务调度。当前裸机的主循环状态机已经能跑,但把采样、显示、存储、通信拆成独立任务,用信号量做同步,代码会清晰很多,不容易出现一个传感器卡死拖垮全部的问题。不过要注意,FreeRTOS的任务栈是RAM里的,F103C8T6只有20KB RAM,任务栈分配要精打细算,不然莫名其妙进HardFault。
第二优先级:添加蓝牙BLE模块,跟手机App联动。BLE模块比ESP8266的好处是功耗低、不需要配网,手机端用调试工具App直接收数据。很多同学在毕设答辩时现场演示WiFi上报会翻车(现场没有WiFi或密码不对),BLE就稳定得多。
第三优先级:加入异常报警逻辑。心率超过阈值、血氧低于90%、体表温度超过38.5℃,都触发蜂鸣器和OLED警示。这部分不复杂,就是一个判断状态机加阈值管理,但非常提升项目的完整度。健康监测如果没有异常预警,说实话就是一个数据仪表,加了报警逻辑才算一个“系统”。
第四优先级:如果对算法感兴趣,可以尝试用递归最小二乘拟合血氧标定曲线,或者用FFT分析心率变异性(HRV)。这些在医疗级算法里都是研究点,靠STM32做FFT计算HRV在计算量上是可行的,但要注意数值精度和内存不足的问题。
我在做这个项目时,最大的体会是:硬件项目的价值不完全在于电路搭得多么炫酷、代码写了多少行,而在于面对那些“文档里不会写”的真实问题时,你怎么定位、怎么修。这些坑——I2C的上拉、DMA的优先级、Flash的对齐、DHT11的优化陷阱——每一个单独看都不大,但累积起来才构成一个工程师真正的手感。
如果你最后也能把传感器数据测准、把系统功耗压下来、把异常状态处理干净,那这个项目对你来说就已经远超“一个毕设”的价值了。
本文还有配套的精品资源,点击获取