搞过几个正经的传感器采集项目之后,我对“基于STM32的心电图监测系统设计”这个题目一直印象很深。它几乎把嵌入式开发里最常碰到的几件事全揉在了一起:模拟小信号采集、ADC采样率规划、数字滤波、实时算法、显示与通信。说白了,这是一个典型的软硬结合项目,不是光会点灯、点串口就能糊弄过去的。
这个题目也是很多毕业设计和课程设计的热门选择,原因很简单——它难度适中、扩展性强、展示效果又特别直观。你把电极往手指上一贴,屏幕或者上位机上立刻跳出一跳一跳的心电波形,心率数字也在实时变,这种“所见即所得”的反馈,比单纯跑个流水灯有说服力得多。我当年做第一版的时候,从零开始到跑出稳定的心率,大概用了两个完整的周末,中间踩的坑不少,但每一个坑都值回票价。
这篇内容不是照着芯片手册念,我会把整套系统从方案选型、硬件搭建、软件滤波到心率识别算法,按我自己实际跑通的思路完整走一遍,最后再附上一份问题排查手册。不管你是准备做课程设计,还是想给stm32项目清单里添一个拿得出手的完整作品,这篇都适合你参考。
1. 系统整体设计:为什么这套组合最适合入门心电项目
1.1 选型思路:STM32F103 + AD8232 为什么是黄金组合
先说结论:主控选 STM32F103C8T6,模拟前端选 AD8232 心电前端模块,这是我见过最稳妥、资料最多、也最适合复现的组合。STM32F103 这个型号大家太熟了,12 位 ADC、3 个片内 ADC 外设、充足的定时器、DMA、USART 全都有,性能对于心电这种低频信号来说绰绰有余。更重要的是它资料丰富,CubeMX 配置起来几小时就能上手,遇到问题搜一下到处都是解决方案。
AD8232 是 ADI 出的一套单导联心电模拟前端方案,芯片内部集成了仪表放大器、运算放大器、右腿驱动电路和导联脱落检测电路。你用三根电极线接到人体,它输出一路已经放大好的模拟心电信号,直接送进 MCU 的 ADC 引脚就行。这比用分立运放自己搭仪表放大器不知道省了多少事,而且芯片的前级和后级滤波电路是固定结构,你只需要按手册选一对电容,就能把通带大致框在 0.5Hz 到 40Hz 附近,正好是心电监测常用的频段。
这个组合还有一个很实际的优点:成本低。主控板加模块加电极片加屏幕,整套物料成本可以控制在百元级别,对预算有限的学生项目非常友好。
1.2 系统架构与信号链路
整套系统可以分成四段来看。
第一段是采集前端,也就是电极和 AD8232 那一块。电极贴到人体皮肤后,心电信号本质上是一个毫伏级别的差分电压信号,幅度大约在 0.5mV 到 4mV 之间。AD8232 负责把这个信号放大到 ADC 合适的量程内,同时完成初步的带通滤波和工频干扰抑制。
第二段是 STM32 核心处理。ADC 以固定采样率采集模拟输出,用 DMA 把数据持续搬到内存缓冲区中,然后在主循环里做数字滤波、R 波检测和心率计算。
第三段是人机交互。可以接一块小尺寸 OLED 或者 TFT 屏显示实时波形和心率数字,也可以把原始数据通过串口发送到上位机,用 Python 自绘波形。
第四段是供电与电源管理。整个系统使用 3.3V 供电,可以由 USB 口直接提供,也可以用锂电池加 LDO 或者 DC-DC 模块供电。
提示:心电监测用于医疗诊断有严格的行业标准,我这个项目定位是学习、原理验证和健康管理参考用途,不承担临床诊断功能。如果你确实奔着医疗级产品去,那要考虑的标准和认证流程是另一个量级的事。
1.3 备选方案对比
如果不是非用 STM32F103 不可,也有几条路线可以对比选择。
| 方案 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| STM32F103 + AD8232 | 资料多、成本低、实现简单 | 模拟前端性能一般,12 位 ADC 精度有限 | 课程设计、入门学习 |
| STM32L4 + ADS1292 | 24 位 ADC、低功耗、信号质量好 | 配置复杂、SPI 驱动工作量较大、成本高 | 想做低功耗产品原型 |
| ESP32 + AD8232 | 自带 WiFi/BLE,无线传输方便 | ADC 精度一般,模拟采集通道少 | 想快速做手机端联网展示 |
| NXP/Freescale KL25Z + 板载传感器 | 官方有完整示例 | 生态相对封闭,资料少 | 学校指定平台 |
我的建议很直接:除非你有明确的低功耗或高精度需求,否则第一版老老实实选 F103 + AD8232。先把信号链路、滤波算法和心率检测这套东西跑通,后面想升级再换平台也不迟。别一开始就挑战高难度配置,模拟前端加 24 位 ADC 再加无线,任何一个环节出问题都会让你怀疑人生。
2. 硬件搭建要点:AD8232 外围电路与信号链设计
2.1 AD8232 模块接口与引脚功能
市面上常见的 AD8232 核心板有十个左右的引脚,功能上有几组要特别注意。供电引脚 VCC 和 GND 负责给芯片供电,工作电压范围大约在 2.0V 到 3.5V,所以直接接 STM32 的 3.3V 是非常合适的,不需要也不应该接 5V 上去。
三个电极接口分别标着 RA(右臂/右手)、LA(左臂/左手)、RL(右腿/参考)。这里面 RA 和 LA 是差分输入的正负端,RL 是右腿驱动输出,主要作用是把人体共模电压反相后反馈回去,抵消掉一部分 50Hz 工频干扰。很多新手图省事,只接两个电极,把 RL 悬空,结果就是波形上叠了一大片锯齿状干扰,怎么滤都滤不干净。这个问题我在后面排查章节还会重点讲。
模块上通常还有 OUT、LO+、LO-、SDN、RDY 这几个引脚。OUT 就是放大后的模拟心电输出,接 MCU 的 ADC 输入引脚。LO+ 和 LO- 是导联脱落检测的输出,可以接普通 GPIO 做提示。SDN 是关断控制脚,低电平有效还是高电平有效要看具体模块丝印,有的板子已经默认拉好了,不用管。RDY 是导联连接状态指示,一般也不需要用。
注意:不同厂家做的 AD8232 模块引脚排列和默认状态不一样,拿到模块第一件事是看丝印、看原理图,确认 SDN 的默认状态,否则可能出现输出一直固定在一个电平上的怪问题。
2.2 电源、去耦与电平匹配
STM32F103 和 AD8232 都要 3.3V 供电。如果用一个 USB 转 TTL 或者 ST-Link 直接给板子 5V,板载稳压器降到 3.3V,那 AD8232 就接 3.3V 输出那一排;如果是自己画板子,建议加一颗低噪声 LDO(比如 AMS1117-3.3 或者更安静的 RT9013),供电端并联一个 10μF 钽电容和一个 0.1μF 陶瓷电容去耦。
还有一个特别实际的细节:心电信号是微伏到毫伏级别的,任何叠加在电源上的纹波都会被 AD8232 当做有效信号放大出来。实测下来,用劣质 USB 充电器供电时,波形底噪明显变大;换一个质量好一点的 5V 适配器,或者干脆用充电宝供电,底噪能下降好几个档次。如果你的波形上有一条很粗的“毛毯”,先别怀疑芯片,先换个供电环境试试。
AD8232 模块输出信号可以直接接到 STM32 的 PA0 或 PA1 上,配置为模拟输入模式即可。不需要经过任何电阻分压或电平转换,因为它们共用一个 3.3V 参考电平。12 位 ADC 的量化精度是 3.3V / 4096 ≈ 0.8mV 每 LSB,再除以 AD8232 约 100 倍的增益,相当于 ADC 对输入端约 8μV 的分辨能力,对于心电 R 波的幅度来说完全够用。
2.3 电极贴合与右腿驱动:抗干扰最关键的一环
这段算是我反复折腾过最久的地方,多说几句。
心电采集的第一道关不是芯片,而是电极和皮肤的接触。本质上,电极片和皮肤之间会形成一层电解质界面,接触阻抗不稳定会产生极化电压,这个偏差电压可能达到几百毫伏,远超心电信号本身。AD8232 虽然靠高通滤波能把极化电压去掉一部分,但前提是电极接触要尽量稳定。
实操时几个有效的做法:贴电极前用酒精棉擦拭皮肤,去掉油脂;电极片按下后稍微等一两分钟让导电膏充分润湿角质层;如果手头没有一次性电极片,用手指按压金属电极片也能凑合,但手指一动信号就跳,只能做静态演示。
右腿驱动的重要性容易被低估。人体在 220V 交流环境中,即使不接触任何带电体,也会通过空间耦合感应出几十伏的 50Hz 共模电压。这个共模电压在差分输入里一般可以被抑制掉,但器件的共模抑制比不是无限的。RL 电极的作用就是把共模分量反相送回人体,主动去抵消它。
我调试时做过一个对比实验:接好 RA、LA,RL 悬空,波形在示波器上像一团乱麻;把 RL 贴上后,毛刺明显收敛,50Hz 成分大幅下降。所以无论你是用模块还是自己画板子,一定把三个电极都接上,尤其是 RL 这根。
3. 软件实现干货:ADC 采样、数字滤波与心率识别算法
3.1 CubeMX 配置与定时器触发 ADC
软件部分我用的是 STM32CubeMX 生成工程,配合 HAL 库。这不是说标准库不行,而是 CubeMX 对 ADC、DMA、定时器这些外设的初始化代码生成得更规范,不容易漏掉配置项。
ADC 采样这一块,我最不建议的做法是在 while 循环里死等转换完成再读值,因为主循环的节奏不受控,采出来的时间间隔抖动大,波形会忽快忽慢,后面做滤波也不好处理。正确做法是用定时器触发 ADC 采样,再用 DMA 把结果自动搬到内存。
以 500Hz 采样率为例,配置思路是这样的:
- 定时器 TIM2 产生 500Hz 的更新事件,通过内部触发信号启动 ADC 转换;
- ADC1 选择定时器触发模式,通道选择 PA0,分辨率 12 位,采样周期不需要太长,心室信号是低频的,几个 ADC 时钟周期就足够;
- DMA 配置为循环模式,每次转换结束自动把结果写入一个数组,数组大小可以设成 1024 或 2048;
- 在 DMA 的半传输完成中断和传输完成中断里设置标志位,主循环检测到标志后处理对应半区间的数据。
用定时器触发 ADC 最大的好处就是采样间隔均匀,这对后面做 50Hz 移动平均滤波尤其重要。窗口长度必须对应固定的时间,抖动太大会严重影响陷波效果。
DMA 的缓冲区分两个半区处理,相当于一个双缓冲机制。当 DMA 正在往后半区写数据时,主循环从容地处理前半区的数据,处理完再等下一个标志,不会出现数据和显示互相踩踏的问题。
3.2 数字滤波实战:50Hz 工频陷波与移动平均
ADC 拿到原始数据后,先别急着算心率,滤波是决定波形能不能看的关键。
心电信号的有效频段大约在 0.05Hz 到 100Hz 之间,但日常监测时我们真正关心的是用于识别 R 波和心率的部分,集中在 0.5Hz 到 40Hz 左右。所以第一件要做的事就是带通滤波,把低频基线漂移和高频肌电噪声都滤掉。
针对 50Hz 工频干扰,有一个非常实用的小技巧:用窗口长度正好为 20ms 的滑动平均滤波器。因为 500Hz 采样率下 20ms 就是 10 个采样点,10 点滑动平均的幅频响应在 50Hz 及其整数倍处会出现陷波,是天然的梳状滤波器。这个滤波器对 QRS 波群来说,10 个点的窗口宽度大约只占整个 QRS 波(80 到 120ms)的十分之一到八分之一,对波形幅度影响很小,但对周期性工频干扰的抑制效果却非常显著。我当时实测,光靠这个移动平均,工频底噪就能被压下去一大半。
滑动平均的实现代码很短:
#define SAMPLE_RATE 500 #define WIN_LEN 10 // 20ms @500Hz int16_t avg_buf[WIN_LEN]; uint8_t avg_idx = 0; int32_t avg_sum = 0; int16_t moving_average_filter(int16_t new_sample) { avg_sum -= avg_buf[avg_idx]; avg_buf[avg_idx] = new_sample; avg_sum += new_sample; avg_idx = (avg_idx + 1) % WIN_LEN; return (int16_t)(avg_sum / WIN_LEN); }如果还想进一步压住更高频的噪声,可以在滑动平均之后再加一个二阶巴特沃斯低通,截止频率设在 40Hz 左右。对于 STM32F103 这种没有 FPU 的 Cortex-M3 而言,浮点运算虽然比 M4 慢,但在 500Hz 采样率下,每个采样点只做几个二阶 IIR 运算,CPU 占用率完全可控,不用担心性能扛不住。
3.3 心率计算:自适应阈值找 R 波
心率计算的常规做法是检测 R 波,测量相邻两个 R 波之间的时间间隔(RR 间期),再用 60 除以间隔时间换算成每分钟心跳数。
R 波是心电波形中幅度最大、斜率最陡的特征波,幅值通常能达到 T 波的三到四倍,所以一个简单有效的方案是:先对滤波后的信号求一个短时能量包络,再拿一个自适应阈值去卡峰值。
具体做法分四步。
第一步,把滤波后的序列取绝对值,再做一个 30ms 左右的滑动平均,得到一条相对平滑的包络线。R 波的突变特性经过这一步会被放大,而 T 波这类缓变波形不容易形成高能量峰。
第二步,设定一个初始阈值。比较省事的方式是取最近一秒内包络最大值的 60% 作为阈值。如果检测到的峰比较少,说明阈值太高,按比例往下调;如果检测到一个时间段内多个峰挤在一起,说明阈值太低,往上调。让阈值跟着信号最大值走,就是自适应。
第三步,加上一个不应期机制。成人正常心率一般不会超过 220 次每分钟,对应的 RR 间期约 273ms。取一个 200ms 的锁定窗口,在检测到一个 R 波后的 200ms 内不再重复检测,这个机制能有效防止同一个 QRS 波被重复计数。
第四步,维护一个 RR 间期数组,每检测到一个新的 R 波,就把当前时间戳减去上次的时间戳存入数组。心率既可以用最近一次 RR 间期算瞬时心率,也可以用最近 8 个 RR 间期的平均值算平均心率,后者更稳定。
R 波检测的核心逻辑可以用伪码表达:
if (envelope > threshold && (now - last_r_tick) >= REFRACTORY_MS) { uint32_t rr_ms = now - last_r_tick; heart_rate = 60000 / rr_ms; // 毫秒换算成 bpm last_r_tick = now; adaptive_update_threshold(envelope); }这个方案对付安静状态下的人体心电信号,准确率相当高。但如果测试者剧烈运动,信号里混入大量运动伪差,准确率会明显下降。想进一步做运动状态下的心率检测,那就得上更复杂的 QRS 检测算法,比如 Pan-Tompkins,那是另一个深水区,这里不展开。
4. 实机调试中的坑:常见问题排查手册
4.1 先看波形再谈算法:调试的必经顺序
我调试这类项目有一条铁律:先把波形调到肉眼看着舒服,再谈心率算法。波形调不好,算法做得再花哨也是空中楼阁。
第一次上电,我是先把 AD8232 的 OUT 脚接到示波器上看原始波形。如果没有示波器,也可以直接把 ADC 原始值打印到串口上位机,把曲线画出来。这一步一定要做,因为你得先确认模拟前端输出正常,ADC 采集的数据有没有进 DMA,滤波有没有把特征波抹掉。
贴好三片电极,坐定不动,示波器上应该能看到类似这样的形态:一次心跳对应一组特征波群,先是幅度较小的 P 波,接着是幅度最大的 R 波,R 波之后跟一个平缓的 T 波,之后是平直的静息段。
4.2 波形问题排查表
把调试过程中最高频的几个现象整理出来,你可以直接按表格对照排查。
| 现象 | 直接原因 | 排查方向 |
|---|---|---|
| 波形是一团乱麻,看不出周期性 | 工频干扰严重、RL 没接好 | 检查三电极是否都贴牢,检查电源质量,尝试用电池供电 |
| 波形缓慢上下飘,基线收不住 | 电极极化电压过大、高通截止频率过低 | 重新清洁皮肤贴电极,检查 AD8232 高通电容值,软件再补一个高通 |
| 波形平线,无任何变化 | 输出脚接错、SDN 被拉低、ADC 配置错误 | 检查 OUT 引脚连接,确认 SDN 默认电平,用万用表量 OUT 引脚电压 |
| 波形有正常形态但幅度巨大、顶部削平 | 增益过高或者输入偏置过大 | 调低增益电阻,检查皮肤接触,确认电极没有贴在一起短路 |
| 波形时有时无,不稳定 | 电极接触松动、导线摆动 | 改用带屏蔽的导联线,固定导线减少摆动 |
注意:如果用的是裸的杜邦线直接连电极,身体稍微一动波形就会剧烈跳变。这种情况下优先怀疑的是电缆引入的噪声,而不是芯片或代码的问题。
4.3 采样与心率算法相关的典型问题
数据采集和算法这一侧的坑,很多是逻辑问题,不是硬件问题。
第一个常见问题是从串口或 OLED 上看到的波形有锯齿状跳变,不像平滑的心电曲线。这通常是采样率设置过低或者 DMA 缓冲区处理不当导致的。心电信号虽然主要能量集中在低频,但 R 波的上升沿其实比较陡峭,采样率低于 200Hz 时波形会明显失真,锯齿感很强。建议至少 500Hz,有条件可以上 1kHz。
第二个常见问题是心率忽高忽低,明明心率 70 左右,读数却能蹦到 140。这通常就是 T 波被误识别成了 R 波。原因是 T 波在滤波后仍然有足够高的幅度,阈值设置的 60% 最大值线被 T 波越过了。解决思路是把阈值系数调高到 70% 左右,同时确保不应期设置合理。T 波和 R 波的时间间隔一般小于 200ms,200ms 的不应期能挡掉大部分 T 波误检。
第三个问题是运动时心率完全测不准。这个我没找到特别完美的纯软件解法,因为运动伪差的频段和心电信号有大量重叠,简单滤波根本分不开。实用思路是加一个加速度传感器做运动伪差检测,检测到运动时就冻结心率显示或者提示“信号受干扰”,好过给用户一个错误的数字。
4.4 没有真实人体信号时的替代调试方案
不是每个人都有条件随时往身上贴电极的。开发阶段可以考虑用一个 10Hz 到 1.5Hz 可调的正弦波信号源代替人体心电信号,直接输入 AD8232 的输入端,验证采集链路和心率算法。AD8232 输入端的共模电平不能悬空,信号源的地要和模块的地相连。
另一个更模拟真实场景的方案是用另一块 STM32 的 DAC 引脚回放一段标准心电数据。把 MIT-BIH 数据库里的一段心电波形采样值存在数组里,每 2ms 通过 DAC 输出一个点,这样就能在桌面上复现一段接近真实形态的心电序列,心率算法就能在没有人体的情况下完成大部分调试。这个思路我强烈推荐,尤其是团队开发时,不可能每轮测试都靠真人贴电极去复现问题。
5. 后续还能怎么玩:从可视化到低功耗与云端
5.1 低功耗与无线采集方向
如果你已经把这个基础版本稳定跑起来了,下一步最自然的升级方向是低功耗和无线化。STM32F103 的低功耗模式虽然不如 L 系列惊艳,但也能做到一定程度的省电。把 ADC 采样改成按需启动,平时让 CPU 进入睡眠,定时唤醒采一段数据再睡回去,用一颗 18650 电池供电撑几个小时做演示是没问题的。
无线方面最简单的路线是加一个 HC-08 或者 CC2541 低功耗蓝牙透传模块,把滤波后的波形数据帧发给手机或电脑。ST 官方有配套的手机 App 可以直接看波形,省得自己写上位机。想做 WiFi 版本的话,把主控换成 ESP32,通过 MQTT 把数据推到云平台,在网页上看实时波形。这种物联网方向在毕业答辩时演示效果很好,评委能看到数据从采集端一路传到云端,链路完整,可说的点也多。
5.2 上位机与心电算法升级方向
另一个很有价值的扩展方向是把重心放到上位机和算法上。STM32 端只做采集和简单滤波,原始数据通过串口或者 USB 发到电脑,在 Python 端完成更精细的处理。Python 生态里处理心电信号非常方便,可以加载 biosppy、wfdb 这些开源库直接做波形分析,画出 P 波、QRS 波群、T 波标注,甚至计算心率变异性(HRV)指标。
我试过这个方案之后最大的感受是:嵌入式端的代码越简单越好,复杂的特征提取交给上位机,迭代速度快得多。你可以先在 Python 里把算法验证清楚,再把确定好参数的滤波器和阈值逻辑降级移植到 STM32 端。这样两端配合,开发效率比自己埋头在嵌入式端反复调参高很多。
从数据存储的角度,如果希望设备独立工作,还可以加一张 microSD 卡,用 FATFS 文件系统记录长时间的心电原始数据。这样不但可以事后回放分析,还能留下自己的实验数据作为研究成果展示。
最后再说两句掏心窝的话
做完这个项目,我最深的体会是:这个题目的难点从来不在 STM32 本身,而在模拟前端和信号质量。MCU 配置、滤波算法、心率计算这些,都是只要有耐心就能搞定的确定性工作;而让三根电极在人身上稳定输出一段干净的心电波形,才是真正考验耐心和排查能力的地方。
拿到板子第一步,不要急着写大段算法,先把供电、电极、参考地这些基础环节做扎实。信号干净了,后面每一步都会变得顺畅。
还有一个小技巧分享给你:每次改动硬件或者调整滤波参数后,把波形截图保存下来,标注当时的工况和参数,积累一段时间以后,这些截图就是最好的调试数据库。哪次波形出问题,翻一翻历史记录,很快就能定位是环境变化还是参数退化。
这套系统的横向扩展能力很强。心电图监测只是心血管健康的一个窗口,同样的采集链路,换一个放大增益和滤波频段,就可以抓脉搏波、呼吸波甚至肌电信号。先把这一套玩明白,再去碰其他生理信号采集项目,你会发现很多东西都是相通的。