简介:这是一份基于STM32的ECG+PPG人体参数测量系统完整设计方案文档,面向嵌入式开发者、物联网学习者及医疗电子方向学生,提供了从硬件选型、模块连接、数据采集到云端部署的端到端工程案例。资源为单份PDF文件,压缩包大小约52.31MB,内容包含系统总体框架与原理图、PulseSensor心率模块、AD8232心电采集模块、ESP8266 WiFi无线传输、华为云IoT平台产品创建与设备注册、MQTT三元组配置及主题订阅发布、Qt上位机环境搭建与UI设计、数据解析更新与实时心电图绘制等关键章节,并附有STM32端KEIL工程说明、硬件连线与取模软件用法。文档篇幅完整且配有硬件实物图、原理图和操作截图,适合希望复现或改造成真实物联网健康监测项目、深入理解MQTT协议与云端对接流程、掌握Qt跨平台上位机开发的读者。已有277人学习下载,可作为课程设计、毕业设计或工程实践的可靠参考。
1. 心电图和光电容积脉搏波,为什么要放在同一套设备里
先说结论:ECG和PPG单独做都很成熟,市面上几十块就能买到单功能的模块,但把它们合到一个基于STM32的板子上再走物联网通道,才是整套设计的价值所在。
心电图(ECG)捕捉的是心肌细胞电活动在体表形成的电位差,波形里有P波、QRS波群、T波,能反映心律失常、心肌缺血等状况。PPG(光电容积脉搏波)则是用LED照射皮肤、通过光电二极管检测血液容积变化,虽然也能算出心率,但从PPG里得不到任何心电波形信息。反过来,ECG标标准准测出心率,却对血氧饱和度(SpO2)无能为力。合在一起,一台设备同时输出心电波形、瞬时心率、血氧饱和度,这就是完整的生命体征监测终端,也正好对应远程健康监测、睡眠监测、老年看护这类真实需求。
华为云IoT在这套系统里的角色是数据出口。MCU端把采集并处理好的数据通过MQTT协议发布到云端,云端负责存储、展示、告警,甚至是后续的AI分析。这样做还有一个实际考量:STM32内部Flash有限,ECG波形如果按250Hz采样、12位精度连续存,几分钟就能写满,必须把原始波形推上云再在服务端处理。
这套项目的目标读者,我理解是两类人:做毕业设计的学生,以及想快速搭一套“传感器+MCU+云平台”参考原型、但不想从零啃协议栈的工程师。下面内容会尽量把链路拆开讲,每个环节都给出我实测过的选型和参数。
2. 硬件链路搭建与器件选型:从电极和光电探头说起
整个硬件链路分四块:ECG模拟前端、PPG探头、主控、通信模块。每一块都有比较成熟的器件组合,我不推荐一开始就追高性能,先把链路跑通再迭代。
2.1 主控选型:为什么STM32F103系列就够用
主控我选择STM32F103RCT6。之所以不直接上F407甚至H7系列,核心原因是ECG和PPG的采样率都不高。心电信号的有用能量集中在0.05Hz到100Hz之间,按奈奎斯特定理加上工程余量,250Hz采样足够;PPG需要的采样率一般也是100Hz量级,MAX30102内部还自带了ADC和FIFO缓冲。整体算下来CPU负载很低,F103的72MHz主频完全跑得动。
同时F103有两个ADC、多个定时器、丰富的DMA通道,ADC采样用定时器触发+DMA搬运,CPU只负责处理数据。通信方面有3个USART,一个接ESP8266,一个留给串口调试,还有一个备用。RCT6的256KB Flash对一套非图形界面固件来说绰绰有余。
如果后续想加TFT屏幕做本地波形显示,建议换成F407VE,多出来的FPU和更大的RAM对刷新波形缓冲区有帮助;不做屏幕的话,F103性价比最高。
2.2 ECG前端:AD8232和ADS1292R怎么选
单导联ECG前端最常见的是ADI的AD8232和TI的ADS1292R。两者路线完全不同。
AD8232是模拟前端,内部集成了仪表放大器、运放、右腿驱动,以及可配置的高通和低通滤波器。输出是模拟信号,直接进STM32的ADC采样。好处是成本低、电路简单,典型应用电路在数据手册里已经给了参考值,适合第一次做心电采集的开发者。
ADS1292R则是24位高精度ADC,内部还集成了呼吸测量功能,输出直接是数字信号,通过SPI读取。指标更漂亮,但BGA封装、SPI时序、寄存器配置都要多花时间。
我实测后建议:如果是偏重信号质量的毕业设计,可以试试ADS1292R;但求稳的话,AD8232更容易一次点亮。这篇内容按AD8232方案展开。
2.3 PPG探头:MAX30102是当前最省事的方案
PPG部分,我调研时对比过很多方案,最终选了MAX30102。这个芯片把红光LED(660nm)、红外LED(880nm)、光电检测器、ADC、环境光抑制、数字滤波全封装在一个5.6mm见方的小模块里。主控只需要通过I2C读写寄存器,再从FIFO里读取数据。
它的寄存器配置要点不少:采样率、LED电流、ADC量程、FIFO采样平均次数都会影响最终信噪比。这个我在第四章展开,先记住一个关键点——红光和红外LED的驱动电流要分开配,不要用同一组值。
2.4 通信模块与电源设计
通信模块用ESP8266系列,通过UART接入。F103没有硬件Wi-Fi,ESP8266是最低成本的联网方案。推荐用ESP-12F模组,Flash更大,AT固件后续升级也方便。
电源部分容易被忽略。ECG是微弱信号测量,电源纹波直接叠加到信号上,所以一定要把数字电源和模拟电源分开。推荐方案:电池或USB的5V进到两个LDO,一个给MCU和外设,另一个单独给AD8232供电。LDO选型上,我用了AMS1117-3.3给数字部分,模拟部分用LP5907,它的噪声指标更低,对心电采集影响小。
3. 从电极到ADC整段走过的ECG信号链路
ECG信号的幅度通常只有0.5mV到2mV,信噪比极低,如果不做处理直接进ADC,基本看不出来波形。AD8232的价值就是把这微弱的差分信号变成1V级别、干净的模拟信号。
3.1 AD8232外围配置参考值和滤波器设计
AD8232的典型应用电路里,决定截止频率的主要是内部放大级和几个外部RC。我用的配置如下:
| 项目 | 参数 |
|---|---|
| 高通滤波器截止频率 | 0.5Hz(避免基线漂移过重) |
| 低通滤波器截止频率 | 40Hz(心电能量集中在0.05~100Hz,40Hz已能保留主要波形) |
| 内部增益 | 约100倍(配合后级可调增益) |
| 右腿驱动 | 开启,抵消共模干扰 |
需要注意的是,40Hz低通和50Hz工频陷波是两个概念。低通只能滤掉高于40Hz的信号,50Hz正好在通带外面一点,但工频干扰往往还带着谐波分量,所以实际使用中还要加一级50Hz陷波,我是在数字域做的,后面会讲。
AD8232的输出引脚OUT通过一个1kΩ电阻串联到STM32的ADC引脚,再对地接一个104电容做RC滤波,这个组合能抑制一部分开关噪声。
3.2 导联脱落检测与右腿驱动
人体处于强电磁环境中,50Hz工频通过空间耦合在人体上产生共模电压,可达几伏甚至十几伏,远超心电信号本身。AD8232解决这个问题的关键引脚是右腿驱动(RLD),它把共模干扰反相放大后再反馈到人体右腿,形成负反馈抵消。
实际连接电极时有两个注意点:一是胸导联的标准位置要保证,电极片和皮肤接触良好,毛发多的地方需要做预处理;二是AD8232的LOFF+和LOFF-引脚可以接出一路导联脱落检测,当某个电极脱落时,模数转换器读到的信号会出现直流偏移。我的固件里每隔一段时间检查这个信号,检测到脱落就通过串口输出告警,避免把无效数据上传到云平台。
3.3 STM32的ADC采样配置:定时器触发+DMA搬运
ADC采样的实现方式,我建议用定时器触发而不是while轮询。轮询采样看似简单,但采样率不稳定,后续波形分析和心率检测的精度都会受影响。
初始化代码关键部分如下:
// 定时器3触发ADC1,采样率250Hz,即4ms触发一次 void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV6; // 12MHz ADC时钟 hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; HAL_ADC_Init(&hadc1); } // 使能DMA,每次转换完成后自动搬到数组 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ecg_raw, ECG_BUF_LEN);DMA搬到的数组是环形缓冲区,每250个点对应1秒数据。心率检测就在这个数组上做滑动窗口处理。采样率250Hz是有意选择的:再高会增加数据量和功耗,对心电频率成分的还原已经足够。
4. PPG信号处理与血氧计算:不是把传感器接到I2C上就完事
MAX30102接入主控在硬件上只要接I2C、中断两个引脚,但真正让它稳定输出有效血氧数据,要调的东西在寄存器里。
4.1 MAX30102寄存器配置与FIFO读取
我用的是推挽中断模式:MAX30102采集完一组数据后拉低INT引脚,STM32检测到下降沿后通过I2C读取FIFO数据。关键配置如下表:
| 寄存器配置项 | 推荐值 | 说明 |
|---|---|---|
| SpO2_ADC_RGE | 2048 nA | ADC满量程,兼顾皮肤光吸收差异 |
| LED_PW | 411μs | 脉冲宽度,越大信噪比越好,功耗也越高 |
| SAMPLE_RATE | 100Hz | 每秒钟100个采样点,血氧计算足够 |
| LED_RED_PA | 15~20 | 红光电流,需要实测调节 |
| LED_IR_PA | 30~40 | 红外电流,一般比红光要大 |
| FIFO_ROLLOVER_EN | 1 | 数据溢出时覆盖旧数据,避免读取错位 |
这里想强调一个实操细节:LED电流不要照抄别人的值。不同肤色、不同测量部位的吸光度差异很大,我在手指测试时红光16、红外32表现最好,但同一个参数换到手腕上波形质量立刻下降。所以固件里最好开一个调试模式,把原始红光和红外波形通过串口发出来,用PC端观察后再动态调整电流。
读取FIFO时用I2C连读6字节,前3字节是红光,后3字节是红外数据。
// 从FIFO读取一组红光/红外数据,18bit有效 uint8_t reg[6]; uint32_t red_led, ir_led; i2c_read_regs(MAX30102_ADDR, REG_FIFO_DATA, reg, 6); red_led = ((reg[0] & 0x03) << 16) | (reg[1] << 8) | reg[2]; ir_led = ((reg[3] & 0x03) << 16) | (reg[4] << 8) | reg[5];注意MAX30102的I2C地址是0x57(7位地址),这在调试时经常被漏看,数据手册写的是“0xAE”8位版本,换算时很容易踩坑。
4.2 血氧饱和度计算原理与实际处理
PPG测血氧的核心原理基于一个事实:氧合血红蛋白(HbO2)和还原血红蛋白(Hb)对红光(660nm)和红外光(880nm)的吸收率不同。红光吸收量随血氧饱和度变化明显,红外光变化较小。计算时先对两路PPG波形做平滑,提取出交流分量(AC)和直流分量(DC),再算比值:
R = (AC_red / DC_red) / (AC_ir / DC_ir)R值代入经验公式可以得到SpO2:
// 这个是常用的经验线性拟合公式,并不是金标准 float spo2 = 110 - 25 * R; if (spo2 < 70) spo2 = 70; if (spo2 > 100) spo2 = 100;代码里的限幅很重要。R值在传感器受到外部光照干扰或手指未放稳时会飞得离谱,如果不做上下限约束,云端收到的可能是几十或者三位数的荒谬值。
心率从PPG信号里计算,逻辑和ECG的R波检测类似,都是找峰值。但PPG容易受运动伪影影响,我处理的简单做法是加一个滑动窗口平均,窗口长度约2秒,峰值间距小于600ms的认为是运动伪影造成的假峰,直接忽略。
5. 通过MQTT把数据送到华为云IoT平台
从STM32到云端的传输路径是:STM32(MQTT客户端)→ ESP8266(Wi-Fi透传)→ 华为云IoT。ESP8266在这里只做UART到Wi-Fi的透传,MQTT协议栈直接在STM32上跑。
5.1 创建产品和鉴权信息的准备
在正式编码前,先在华为云IoT平台完成三件事:创建产品、注册设备、定义产品模型。产品模型里的服务属性建议这样设计:
| 属性名 | 数据类型 | 说明 |
|---|---|---|
| heart_rate | int | 心率,单位bpm |
| spo2 | int | 血氧饱和度,单位% |
| ecg_wave | decimal数组 | 心电波形,上传一段原始采样 |
| timestamp | string | 采集时间 |
设备注册后会拿到设备ID(device_id)和设备密钥(secret)。HTTP推送参考文档里,MQTT连接时的关键参数是这些:
- 接入地址:
iot-mqtts.cn-north-4.myhuaweicloud.com,端口8883(加密)或1883(明文) - ClientId:
{device_id}_0_0_timestamp - Username:
{device_id} - Password:通过HMAC-SHA256计算,密钥是设备密钥,数据是timestamp
在STM32端做HMAC-SHA256有点重量级,我的做法是先用上位机或脚本算好密码,然后直接把这个hex字符串烧进固件。一次性算好、固定使用,省去了在MCU上移植mbedtls的工作。
5.2 数据上报的Topic与JSON报文格式
华为云IoT平台设备上报属性的Topic格式是:
$oc/devices/{device_id}/sys/properties/report报文是JSON,格式如下:
{ "services": [ { "service_id": "health_monitor", "properties": { "heart_rate": 76, "spo2": 98, "timestamp": "2024-05-20 10:30:00" } } ] }STM32没有现成的JSON库,直接引入cJSON。这没什么好避讳的,cJSON的源码就两个文件,移植成本很低,而且用起来非常顺手。
cJSON *root = cJSON_CreateObject(); cJSON *services = cJSON_AddArrayToObject(root, "services"); cJSON *service = cJSON_CreateObject(); cJSON_AddItemToArray(services, service); cJSON_AddStringToObject(service, "service_id", "health_monitor"); cJSON *props = cJSON_AddObjectToObject(service, "properties"); cJSON_AddNumberToObject(props, "heart_rate", heart_rate); cJSON_AddNumberToObject(props, "spo2", spo2); char *payload = cJSON_PrintUnformatted(root); // 通过MQTT publish到report topic把cJSON分配内存的释放流程理清楚,不要重复释放。由于云端属性定义的是单个数值,不需要把整段ECG波形每个点都发上云,那样报文太大、流量消耗也快。我的做法是只发经过处理后的特征,比如心率、血氧,每5秒上报一次。
5.3 断线重连与心跳保活
物联网设备最尴尬的问题就是网络闪断。Wi-Fi覆盖不稳定、AP重启、云端会话超时,任何一个原因都可能导致断线。我在固件里做了一个简单的状态机:正常上报、连接断开、尝试重连、重新订阅。每次MQTT连接时设置keepalive为60秒,平台发现连接超过这个时间没有报文,会主动断开。所以固件里要保证60秒内至少有一次上报或ping报文。
实际测试中,ESP8266透传模式有个坑:长时间不通后,突然来数据大概率会发不出去。解决办法是每5秒发一条自定义AT指令检查链路,比如AT+CIPSTATUS,如果响应不是“CONNECTED”就重启ESP8266的TCP连接。这个探测开销很低,但对稳定性提升明显。
6. 实测踩坑记录:从“No target found”到波形毛刺
项目做完回过头看,大坑小坑至少踩了七八个,这里挑几个影响最大的记录一下,说不定能帮后来者省几天时间。
6.1 ST-LINK报“No STM32 target found”的排查
这个问题几乎是每个STM32初学者都会遇到的。我当时在配置完工程后点下载,立刻弹出这个错误,第一反应是驱动问题,换了好几个版本的ST-LINK Utility都没用。后来才排查到真正的原因:给目标板供电的是USB转串口模块的3.3V,而这个模块在上电瞬间电压建立太慢,导致SWD接口时序错乱。
解决方法是先给目标板上电,等电压稳定后再在IDE里点下载。如果还不行,检查SWDIO和SWCLK是不是接反了,以及目标板的复位电路上是不是有一颗大电容拉长了复位时间。用橡皮擦清理一下ST-LINK的排针触点也有效,这个操作听起来不靠谱,但我真的遇到过氧化导致接触不良的情况。
6.2 ECG波形上的50Hz毛刺和基线漂移
第一次看到AD8232输出的波形时,QRS波群被50Hz干扰包得几乎看不出来。我在示波器上看到的不是干净的PQRST波形,而是带着厚厚毛刺的信号。
先做了模拟域的整改:右腿驱动一定要接,而且右腿电极和测量电极不在同一个位置;模拟地和数字地在PCB上单点连接,不要在PCBA上飞来飞去。这些都做了之后,毛刺还有残余,最后又在数字域补了一招——软件陷波器。
因为采样率固定250Hz,一个50Hz的IIR陷波器可以这样实现:
// 50Hz数字陷波,采样率250Hz,Q=10 float notch_filter(float input) { static float x1 = 0, x2 = 0, y1 = 0, y2 = 0; float b0 = 0.9005f, b1 = -0.5569f, b2 = 0.9005f; float a1 = -0.5569f, a2 = 0.8010f; float y = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; x2 = x1; x1 = input; y2 = y1; y1 = y; return y; }陷波器会稍微影响心电波形,但主要特征波形的形态变化很小,可以接受。
6.3 血氧数据跳变和设备在云端的影子状态不一致
MAX30102最常出现的问题就是血氧突然变成99%然后跳到88%,这种跳变在小范围波动时还好,但如果出现在睡眠监测场景,会直接触发云端告警。
这个问题的根源是两个:一是运动伪影,二是LED电流设置不合适。我最终的解决策略是多层组合:读取MAX30102内部的高环境光消除寄存器,增强抗干扰能力;软件上不采用单点计算,而是把最近8个周期的R值做滑动平均;超过20%的突变直接丢弃,用上一次正常值替代。
云端测试时还发现一个问题:设备成功上报后,IoT平台设备列表仍显示“未激活”。排查到最后是时间戳的问题。平台要求报文里的时间戳字符串格式必须是yyyy-MM-dd HH:mm:ss,而我一开始传的是时间戳数字。改成字符串格式后再上报,状态立即刷新。
6.4 实测数据参考
项目完成后,我在安静办公室环境用这套系统做了几组简单测试,数据仅供读者参考:
| 测量项 | 实测结果 | 对照设备 |
|---|---|---|
| 心率 | 68~72 bpm(稳定状态) | 指尖医疗级血氧仪,偏差≤2bpm |
| 血氧饱和度 | 97%~98% | 同款血氧仪,偏差≤1% |
| ECG波形 | 可见清晰P波、QRS波、T波 | 和BOSS图对比形态一致 |
| 数据上报成功率 | 99.6%(连续12小时) | 华为云IoT控制台统计 |
这套系统在安静环境下达到医疗级别精度比较困难,但作为健康趋势监测和教学演示已经很合格了。连续运行12小时的可靠性,对于毕业设计答辩或者产品Demo来说足够。
最后分享一个我个人调试时的小习惯:所有关键数据点都打上串口日志,用带时间戳的输出把“MCU采集时间”和“云平台接收时间”对齐。这样不管是硬件问题还是网络问题,都能快速定位到底发生在哪一段链路。ECG和PPG双模采集加上云平台的整套方案,真正难的不是某个单项技术,而是把微弱信号采集、嵌入式算法、物联网通信这些环节串起来之后,还能稳定地长期运行。
本文还有配套的精品资源,点击获取