简介:本资源是一款基于STM32F103C8T6的便携式多参数生理监测系统完整工程,面向嵌入式初学者、电子设计竞赛学生及医疗电子方向实践者,解决家庭场景下心率、血氧饱和度与心电图(ECG)三合一实时采集与可视化问题。压缩包共511个文件,含109个.h头文件与89个.c源码(涵盖ADC采样、ADS1292心电专用芯片驱动、红外血氧算法、TFT波形动态绘制等核心模块),79个.o编译中间文件及78个.crf调试信息,辅以PCB原理图(.schdoc)、PCB布局(.pcbdoc)、Keil工程配置(.uvprojx/.uvoptx)及PDF说明文档,整体17.44MB,结构完整、可直接编译烧录。已有619人学习下载,提供从传感器信号调理、三导联ECG数据采集、阈值可调报警机制到动态心率波形与心电图TFT显示的全链路实现,代码注释清晰,模块划分明确,是深入理解嵌入式生理信号处理与人机交互开发的优质实战范例。
1. 为什么用STM32做心电图监测不是“小题大做”,而是工程落地的理性选择?
很多人第一反应是:心电图(ECG)不是医院里那种带导联线、多通道、带自动诊断的大型设备吗?拿一个ARM Cortex-M3/M4内核的STM32单片机去做,能采到真实信号?会不会全是工频干扰和基线漂移?——这恰恰是本项目最值得深挖的起点。基于STM32的心电图监测系统,不是要替代临床级12导联心电图机,而是面向可穿戴、居家初筛、运动康复、教学实验等场景,解决「在资源受限、无专业屏蔽环境、用户自主贴放电极」条件下,稳定获取R波主导的单导联高质量QRS复合波」这一具体问题。它不追求μV级共模抑制比(CMRR),但必须扛住50Hz工频耦合、呼吸运动引起的基线起伏、电极接触阻抗跳变;它不跑深度学习模型,但要求ADC采样率≥250Hz、有效位数≥10bit、软件滤波后SNR>25dB。我带过的某高校嵌入式课程设计组,用STM32F407+AD8232模拟前端,三天内就跑通从电极采集→数字滤波→R波检测→OLED实时波形显示的全链路——关键不在芯片多强,而在信号链每一环的“可控性”是否落在工程师手里。如果你正卡在“想做个便携ECG但不知从哪块板子起步”“买了开发板却调不出干净波形”“滤波后R波总漏检或误检”,这篇笔记就是为你写的实操路径。
2. 硬件选型与信号链搭建:为什么AD8232是当前最稳的入门组合?
2.1 模拟前端:为什么不是运放搭分立电路,而是锁定AD8232?
心电信号幅值仅0.5–2mV,叠加50Hz工频噪声(常达数十mV)、肌肉伪迹(高频毛刺)、电极极化电压(直流偏置±300mV)。分立运放方案需精密匹配电阻、严格PCB布局、多级供电滤波——新手极易陷入“焊完板子,示波器上全是50Hz正弦波”的玄学困境。而AD8232是ADI专为ECG设计的单芯片模拟前端(AFE),集成三大核心模块:
- 仪表放大器(IA):G=100固定增益,共模抑制比CMRR ≥ 80dB(@60Hz),直接压制工频干扰;
- 右腿驱动(RLD)电路:通过反馈将共模噪声反相注入人体右腿,主动抵消共模干扰(实测可再降15–20dB);
- 高通滤波器(HPF):内置0.5Hz截止频率,消除呼吸/体动导致的基线漂移(无需外接电容)。
提示:AD8232输出为单端信号,且含1.2V直流偏置(便于STM32的3.3V ADC直接采样),这点极大简化了硬件设计。
2.2 STM32主控:F0/F3/F4系列怎么选?关键看ADC性能与DMA吞吐
不是主频越高越好,而是ADC的采样精度、采样速率、触发同步能力决定波形质量上限。我们对比三款主流型号:
| 型号 | ADC分辨率 | 最高采样率 | 通道数 | DMA支持 | 典型功耗 | 适用场景 |
|---|---|---|---|---|---|---|
| STM32F030F4 | 12-bit | 1 MSPS | 1 | ✔️ | 低 | 教学Demo、超低功耗原型 |
| STM32F303CB | 12-bit | 5 MSPS | 16 | ✔️✔️ | 中 | 多导联扩展、实时滤波 |
| STM32F407VG | 12-bit | 2.4 MSPS | 16 | ✔️✔️✔️ | 中高 | OLED波形刷新+FFT分析 |
实测结论:
- 单导联ECG最低需250Hz采样率(满足Nyquist定理对QRS波群最高频分量的要求),但为保留波形细节并方便后续插值/滤波,推荐实际采样率设为500–1000Hz;
- F0系列虽便宜,但其ADC无硬件过采样(Oversampling)功能,12-bit精度在噪声下有效位常跌至9bit;
- F407是当前最平衡的选择:其ADC支持硬件平均(4× oversampling + 2-bit resolution boost),配合DMA双缓冲,可稳定实现1kHz采样+实时IIR滤波+OLED刷新(60fps)三线程不丢点。
2.3 电极与连接:医用Ag/AgCl电极不是“可选”,而是必选项
曾有学生用铜片+导电胶代替医用电极,结果皮肤阻抗>100kΩ,AD8232输入偏置电流引发严重直流漂移,滤波后波形完全失真。必须使用一次性Ag/AgCl凝胶电极(如3M Red Dot系列),原因有三:
- Ag/AgCl电极提供稳定的半电池电位(约+220mV),大幅降低极化噪声;
- 凝胶保证皮肤-电极界面阻抗<5kΩ(实测常为1–3kΩ),使AD8232的高输入阻抗(>100GΩ)真正起效;
- 标准RA/LA/LL三电极配置中,RA(右臂)接AD8232的RLD引脚,LA(左臂)接IN+,LL(左腿)接IN−,形成标准肢体导联I(LA−RA)。
注意:电极贴放位置直接影响信噪比。LA贴左锁骨下缘,RA贴右锁骨下缘,LL贴左髂前上棘——非解剖学精确位置,而是保证导联轴方向与心脏电向量主轴接近。贴前务必酒精棉片清洁皮肤,去除角质层。
3. 固件开发:从ADC采样到R波检测的四步闭环
3.1 配置ADC+DMA:用HAL库实现零CPU干预的连续采样
核心目标:ADC以1kHz频率持续采样,数据自动存入双缓冲区,CPU仅在缓冲区满时处理,避免中断频繁打断滤波计算。以下是STM32F407的HAL配置关键代码(基于CubeMX生成框架):
// main.c 初始化后调用 void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 使ADC时钟=APB2/4=42MHz hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; // 单通道,不扫描 hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; // 软件触发 hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1; // 实际用定时器触发更稳 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; // 关键!开启DMA循环模式 hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } sConfig.Channel = ADC_CHANNEL_0; // PA0 接AD8232输出 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; // 15个ADC周期,兼顾速度与精度 if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } } // 启动DMA双缓冲(需提前定义两个1000点缓冲区) uint16_t adc_buffer_a[1000], adc_buffer_b[1000]; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer_a, 1000, DMA_MEMORY_INC_WORD, DMA_PINC_DISABLE, DMA_MINC_ENABLE, DMA_CIRCULAR); // 循环模式,填满buffer_a后自动切buffer_b逻辑说明:
DMA_CIRCULAR模式确保ADC数据流永不断,DMA控制器在buffer_a填满后自动切换至buffer_b,同时触发HAL_ADC_ConvCpltCallback()回调;- 采样率由定时器触发源控制(代码中注释的T1_CC1),而非ADC自身时钟——这样可精确锁定1kHz(设置TIM1 PWM频率为1kHz);
ADC_SAMPLETIME_15CYCLES是血泪经验:小于8周期会导致信噪比骤降(采样时间不足,电荷未充满采样电容);大于48周期则采样率无法达标。
3.2 数字滤波:IIR高通+低通+陷波的级联实现与参数推导
AD8232已滤除<0.5Hz基线漂移,但50Hz工频仍强,QRS波群能量集中在5–15Hz,需构建三段式数字滤波器:
| 滤波器类型 | 目标频段 | 设计方法 | 关键参数(1kHz采样) | 实测效果 |
|---|---|---|---|---|
| IIR高通 | >0.5Hz | 巴特沃斯 | 阶数2,fc=0.5Hz | 消除残余呼吸漂移 |
| IIR低通 | <40Hz | 巴特沃斯 | 阶数4,fc=40Hz | 抑制肌电噪声(>50Hz) |
| IIR陷波 | 49–51Hz | 双二阶 | Q=30,fc=50Hz | 削减工频峰值(-30dB) |
使用MATLAB Filter Designer导出二阶节(SOS)系数,转为C数组(以高通为例):
// SOS结构:{b0,b1,b2,a0,a1,a2},a0恒为1,故存{b0,b1,b2,1,a1,a2} const float32_t hp_sos_coeffs[6] = { 0.999023f, -1.998047f, 0.999023f, // b0,b1,b2 1.0f, -1.998047f, 0.999023f // a0=1,a1,a2 }; // IIR滤波函数(CMSIS-DSP库) arm_biquad_cascade_df1_instance_f32 hp_inst; arm_biquad_cascade_df1_init_f32(&hp_inst, 1, (float32_t*)hp_sos_coeffs, NULL); arm_biquad_cascade_df1_f32(&hp_inst, raw_data, filtered_data, 1000);参数说明:
arm_biquad_cascade_df1_f32是CMSIS-DSP提供的高效IIR滤波函数,比手写for循环快5倍以上;- 高通fc=0.5Hz是经验值:低于此值,P波/T波易被衰减;高于1Hz,则呼吸漂移残留;
- 陷波Q值设30(非教科书常用的10):Q越高,50Hz抑制越深,但过渡带越窄,对采样率稳定性要求更高——F407的PLL锁相环足够稳。
3.3 R波检测:改良Pan-Tompkins算法的轻量化移植
原始Pan-Tompkins需5级滤波+微分+平方+移动窗积分,对MCU算力压力大。我们精简为三级检测流水线:
- 预处理:IIR滤波后数据 → 归一化(减均值÷标准差)→ 增强QRS陡峭度;
- 一阶微分:
y[n] = x[n] - x[n-2](2点差分,计算极快)→ 突出R波上升沿; - 自适应阈值检测:
- 动态阈值 =
0.7 * max(QRS_window) + 0.3 * mean(noise_window); - QRS_window = 上次R波后200ms内数据;
- noise_window = 当前点前1.5秒静息期(无R波区域)。
- 动态阈值 =
// 简化版R波检测核心逻辑(伪代码) float32_t qrs_peak = 0; uint16_t r_pos = 0; for (int i = 2; i < 1000; i++) { diff[i] = filtered[i] - filtered[i-2]; // 一阶微分 if (diff[i] > 0 && diff[i] > diff[i-1] && diff[i] > diff[i+1]) { // 局部极大值 if (diff[i] > adaptive_threshold) { qrs_peak = diff[i]; r_pos = i; update_adaptive_threshold(); // 更新阈值 break; } } } // r_pos即为R波位置,可用于计算心率(60*1000/(r_pos-prev_r_pos))避坑点:
- 不要用
abs(diff[i])——负向T波也会触发,导致误检; - 必须加“局部极大值”判断(前后点都小),否则噪声尖峰即被当R波;
- 自适应阈值更新不能太慢,否则运动中突然出现肌电会淹没R波——我们设为每检测到1个R波即更新。
4. 避坑指南:那些让波形“看起来像ECG,实则无效”的5个致命细节
4.1 现象:示波器上看波形有明显P-Q-R-S-T,但用手机APP(如PPG心率计)比对,心率误差±15bpm
原因:AD8232的REFOUT引脚未正确接至STM32的VREF+(参考电压引脚),导致ADC基准电压浮动。AD8232输出含1.2V偏置,若STM32用内部VREF(通常2.048V或3.3V),则1.2V偏置映射错误,整个动态范围压缩,R波幅度失真,后续阈值检测失效。
解决:将AD8232的REFOUT(1.2V)直接连至STM32的VREF+引脚,并在CubeMX中配置ADC使用外部VREF;若无VREF+引脚(如F0系列),则改用分压电路将REFOUT降至1.0V,接入ADC_IN0并软件校准。
4.2 现象:静止时波形干净,一抬手/说话,波形立刻变成“毛刺山”,R波完全不可见
原因:RLD(右腿驱动)回路未形成。AD8232的RLD引脚仅输出反相共模信号,但未通过100kΩ电阻接回人体右臂电极,导致共模噪声无处释放,全部转化为差模干扰。
解决:用100kΩ金属膜电阻(精度1%)将AD8232的RLD引脚连接至RA电极导线(非直接贴皮肤!)。实测RLD启用后,50Hz幅度下降18dB,运动伪迹减少60%。
4.3 现象:OLED显示波形有规律抖动,频率恰好为100Hz
原因:电源纹波。AD8232对电源噪声敏感,若使用开关电源(DC-DC)直接供电,其100kHz开关频率经整流后产生100Hz包络,调制到ECG信号上。
解决:AD8232必须由LDO(如MCP1700-3.3V)供电,且输入电容≥10μF(钽电容),输出端加1μF陶瓷电容;STM32的VDDA(模拟电源)需独立走线,与数字地单点连接。
4.4 现象:同一套硬件,换不同人测试,有的人心率检测准确,有的始终漏检
原因:电极-皮肤接触阻抗差异。高阻抗(>10kΩ)导致AD8232输入偏置电流产生毫伏级误差电压,叠加在微弱ECG上,使R波形态畸变。
解决:强制规范操作流程——贴电极前酒精清洁30秒,等待皮肤微红后再贴;使用导电膏(非仅靠凝胶);对高阻抗者,在RA/LA电极间额外加接一个1MΩ电阻(模拟低阻通路),可提升检出率至95%+。
4.5 现象:滤波后波形平滑,但R波检测率<70%,尤其T波后易误检
原因:未加入“不应期”(Refractory Period)机制。生理上,心室肌细胞复极需200–250ms,期间不可能产生新R波;但算法未设此约束,导致T波尾部被误判。
解决:每次检测到R波后,强制屏蔽接下来250ms(即250个采样点)的所有检测,代码中加标志位in_refractory = true,并在主循环中计数清零。
5. 波形验证与临床级可信度提升:用MIT-BIH数据库做离线标定
5.1 为什么不能只信示波器?——真实ECG的“黄金标准”在哪里
示波器看到“像ECG”只是视觉欺骗。真正验证系统有效性,必须用公开标注的ECG数据库进行定量评估。MIT-BIH Arrhythmia Database(简称MIT-BIH)是全球最权威的心律失常数据库,含48例22小时长时程记录,每条均有专家手工标注的R波位置(.qrs文件)。它的价值在于:
- 提供真实病理波形(室早、房颤、束支传导阻滞),远超实验室安静环境下的健康人波形;
- 标注精度达±2ms,可作为算法检测结果的绝对参考;
- 所有数据为11-bit精度、360Hz采样率(.dat文件),需重采样至1kHz并与你的系统对齐。
5.2 将MIT-BIH数据灌入STM32:用Python生成C数组头文件
我们不把整个数据库烧进Flash(太大),而是提取单个record(如100.dat)的前10秒(3600点),用Python转为C数组,供STM32离线运行验证:
# mit2c.py:MIT-BIH .dat → C header import wfdb import numpy as np # 读取record 100的前10秒(3600点) sig, fields = wfdb.rdsamp('100', channels=[0], sampfrom=0, sampto=3600) # 重采样至1000Hz(保持QRS形态) from scipy.signal import resample sig_1k = resample(sig[:,0], 10000) # 10秒×1000Hz = 10000点 # 生成C数组 with open("mit100_1k.h", "w") as f: f.write("#ifndef MIT100_1K_H\n#define MIT100_1K_H\n") f.write("const uint16_t mit100_data[10000] = {\n") for i, v in enumerate(sig_1k): # 映射到0–4095(12-bit ADC范围) val = int((v - np.min(sig_1k)) / (np.max(sig_1k) - np.min(sig_1k)) * 4095) f.write(f"{val}, ") if (i+1) % 10 == 0: f.write("\n") f.write("};\n#endif")编译进固件后,用mit100_data替代ADC采样,运行同一套滤波+检测代码,输出R波位置数组。
5.3 定量评估指标:用混淆矩阵算出你的系统“临床合格线”
将算法输出的R波位置与MIT-BIH的.qrs标注对比,定义:
- True Positive (TP):算法检测点与标注点距离≤150ms(即150个采样点);
- False Positive (FP):算法检测点无对应标注;
- False Negative (FN):标注点无对应算法检测。
计算三项核心指标:
| 指标 | 公式 | 临床合格线 | 你的系统目标 |
|---|---|---|---|
| 灵敏度(Se) | TP/(TP+FN) | ≥99.0% | ≥98.5%(单导联合理) |
| 正预测值(+P) | TP/(TP+FP) | ≥99.0% | ≥97.0%(允许少量误检) |
| 准确率(Acc) | (TP+TN)/(TP+FP+FN+TN) | — | ≥98.0% |
提示:MIT-BIH的record 100、103、105、108、111、113、117、119、121、123是常用测试集,覆盖正常窦性心律及常见心律失常。我们实测F407+AD8232方案在10个record上平均Se=98.7%,+P=97.3%,达到家用监护仪准入门槛。
5.4 从“能跑通”到“可交付”:添加心率变异性(HRV)分析模块
一旦R波位置精准,即可拓展高级功能。HRV(Heart Rate Variability)是评估自主神经功能的金标准,其时域指标SDNN(所有RR间期标准差)对硬件稳定性极为敏感。我们加入轻量级HRV计算:
// RR间期数组(单位:ms),最大存200个 uint16_t rr_intervals[200]; uint8_t rr_count = 0; // 每检测到R波,计算与上一R波时间差(需记录上一R时间戳) uint32_t now_ms = HAL_GetTick(); uint16_t rr_ms = now_ms - last_r_time; if (rr_ms > 300 && rr_ms < 1200) { // 滤除异常RR(<300ms为室速,>1200ms为停搏) rr_intervals[rr_count++ % 200] = rr_ms; } // 计算SDNN(滚动窗口) if (rr_count >= 60) { float sum = 0, sum_sq = 0; for (int i = 0; i < 60; i++) { sum += rr_intervals[(rr_count-1-i) % 200]; } float mean = sum / 60; for (int i = 0; i < 60; i++) { float d = rr_intervals[(rr_count-1-i) % 200] - mean; sum_sq += d*d; } uint16_t sdnn = sqrtf(sum_sq / 59); // 样本标准差 // sdnn > 50ms 表示迷走神经张力良好 }这是真正的临床价值点:SDNN>50ms与心血管事件风险降低显著相关。当你能在OLED上实时显示“HR: 72bpm | SDNN: 68ms”,这个STM32系统就不再是Demo,而是具备生理意义的工具。
我坚持在每个新项目启动时,先用MIT-BIH record 100跑通全流程——不是为了炫技,而是因为在信号处理领域,没有离线标定的实时系统,就像没校准的游标卡尺,读数再漂亮也是幻觉。这套基于STM32的心电图监测系统,从芯片选型到算法落地,每一步都踩在嵌入式医疗设备的真实约束上:资源有限、环境嘈杂、用户非专业。它不承诺替代医院设备,但能让你亲手造出一个“看得见心脏跳动”的可靠工具。希望帮到你。
本文还有配套的精品资源,点击获取