简介这是一套完整的基于STM32F103与AD8232的心电信号采集系统项目面向嵌入式开发者和生物医学工程学习者解决从模拟心电信号获取、微控制器处理到上位机波形显示的全链路实现难题覆盖硬件接口设计、USART2串口通信和信号处理算法等关键环节。包内共191个文件以C语言源码.c/.h和工程配置文件为主包含Keil项目.uvprojx/.uvoptx可直接打开编译附带编译生成的.hex/.axf固件、调试记录及辅助脚本压缩包仅3.74MB目录结构清晰便于按模块查阅与移植。已有902人学习项目将AD8232模拟前端输出接入STM32的PA3引脚通过USART2与vofa上位机交互实现心电波形实时显示体现了GPIO、定时器、串口中断、数据滤波等知识点的综合应用。开发者可在源码中学习AD8232的滤波放大配置、串口协议的具体定义和上位机数据解析流程也能参考其整体工程组织方式作为医疗电子或便携式生理信号监测项目的基础模板。1. 心电采集看着简单难点全在模拟前端一张波形图说服你第一次给这套基于STM32F103的心电信号采集系统上电时我满怀信心ADC读数据、串口发曲线这不是嵌入式入门基本功吗结果屏幕上显示的是一条约 50Hz 的粗正弦线幅度几十毫伏中间完全找不到 P、QRS、T 波的影子。后来我才意识到心电采集真正磨人的地方不在单片机而在模拟前端和数字滤波——传感器贴在人身上人体就是天线工频干扰、肌电噪声、呼吸引起的基线漂移会一并混进来。这套系统实现的是一条完整的低成本链路仪表放大器做差分拾取右腿驱动抑制共模干扰带通滤波器做频带整形STM32F103 的定时器触发 ADC 以 500Hz 采样DMA 循环搬运再在单片机里做 50Hz 陷波和基线漂移抑制。适合的人群很明确做便携健康设备的预研、低成本原型验证、相关方向的课程设计。读完之后你能清楚知道每级放大该给多少增益、采样率为什么取 500Hz、数字滤波的系数怎么算以及那几个最容易翻车的细节。2. 模拟前端设计心电采集的真正门槛2.1 仪表放大器加右腿驱动把差分信号从共模噪声里捞出来心电信号在体表只有 0.54mV而人体同时携带幅度可达几伏的 50Hz 共模干扰。想要从这么大的噪声里捞出有用信号第一级必须用仪表放大器而不是普通运放。仪表放大器最大的优势是差分输入、高共模抑制比CMRR它只放大两个电极之间的电压差对两个电极上同时出现的共模信号天然免疫。增益通过一个外部电阻 RG 设定常见仪表放大器的计算公式为 G 1 49.4kΩ / RG。我一般把第一级设在 20 倍RG 取 2.61kΩ 标称电阻这样既不会因为增益太高导致输出饱和又能在后级继续做频率整形。这里有个容易忽略的细节仪表放大器的参考端REF不是随便接地的它决定了输出直流电平。我会把 REF 接到一个 1.65V 的偏置上让输出中点落在 ADC 量程的中间避免信号只在一半幅度内摆动。右腿驱动是抑制共模干扰的关键一招。简单说就是把两个输入电极的共模电压取出来反相放大之后反馈到人体右腿形成一个负反馈环路把人体上耦合进来的共模电压压下去。具体实现是用两个等值电阻比如 100kΩ从输入端取共模中点送入反向放大器输出经过一个 100kΩ 限流电阻接到右腿电极。这个环路的增益不需要太高1050 倍就够了太高容易振荡。2.2 带通整形与主放大把频带和幅度送到 ADC 的甜点区心电信号的频率范围大约在 0.05100Hz其中 QRS 波群的能量集中在 10Hz 附近。模拟前端需要把带外噪声压掉高通用于去掉电极极化电压和呼吸引起的超低频漂移低通用于滤掉肌电和部分高频噪声。我常用的做法是仪表放大器输出后接一阶 RC 高通C 取 1μF、R 取 1.6MΩ截止频率约 0.1Hz再接一个 Sallen-Key 结构的二阶低通R 取 100kΩ、C 取 10nF截止频率约 159Hz。主放大器放在滤波之后增益取 50 倍。这样整个链路总增益是 20 × 50 1000 倍1mV 的心电信号到 ADC 输入端就变成 1V。STM32F103 的 ADC 参考电压是 3.3V1V 幅度的信号大概占量程的 30%既不削顶也不浪费动态范围。如果你的电极贴得更紧、信号更大可以把主放大增益往下调如果被测者体脂偏厚、信号偏弱就需要往上调。这个增益不是死的最好做成可调。2.3 分立方案还是专用模拟前端芯片两条路怎么选自己搭仪表放大器加运放方案成本低、器件常见方便理解每一级的工作点调试时也能用示波器逐级定位问题特别适合原型验证和教学场景。缺点是体积大、功耗高、电磁兼容要靠自己调量产一致性也差一些。如果目标直接是产品化我更建议换成专用模拟前端芯片。这类芯片把仪表放大器、右腿驱动、滤波器、24 位 ADC 全部集成在一起有的还内置了导联脱落检测和威尔逊中央电端STM32F103 只需要通过 SPI 把数据读出来就行。开发时间能省一大半功耗也低一个数量级。对比项分立仪表放大器方案专用模拟前端芯片方案成本低中开发难度高需调增益和滤波低寄存器配置为主体积与功耗大、高小、低学习价值高能摸到每个环节中偏应用适合阶段原型验证、课程设计预研转产品、量产3. 定时器触发 ADC 多通道采集采样率怎么定DMA 怎么收3.1 采样率 500Hz 和 12 位分辨率够用且留了余量先算一笔账。心电诊断设备常把带宽做到 0.05100Hz根据奈奎斯特采样定理采样率至少是最高频率的两倍也就是 200Hz。实际工程里不会贴着理论值跑我习惯取 2.5 倍余量定在 500Hz也就是每毫秒采一个点。这个频率既能保住 QRS 波的形态细节又不会给 STM32F103 带来太大的计算压力。分辨率方面STM32F103 的 ADC 是 12 位在 3.3V 参考下每个最小量化步长约 0.8mV。看起来不怎么样但别忘了前面还有 1000 倍模拟增益折算到输入端等效分辨率约 0.8μV对心电采集来说足够了。过度追求高分辨率没有意义模拟前端的噪声电平往往比量化噪声高得多。3.2 定时器 TRGO 触发 ADC 加 DMA 循环存储完整配置手动在循环里触发 ADC 的问题是采样间隔受主循环代码影响抖动很大波形会失真。正确做法是用定时器产生固定频率的触发信号接 ADC 的外部触发引脚ADC 完成转换后自动把结果搬运到内存全程不需要 CPU 干预。以下配置基于标准外设库先看定时器void TIM3_Trigger_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period 2000 - 1; // 1MHz / 2000 500Hz 采样率 tim.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tim); // TIM3 更新事件作为 TRGO触发 ADC 转换 TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); }核心逻辑是让 TIM3 每 2ms 产生一次更新事件通过 TRGO 引脚通知 ADC 启动一次转换。预分频 71 把 72MHz 系统时钟降到 1MHz重装值 1999 再分频得到 500Hz。想改采样率只需调整这两个值比如把重装值改成 999采样率就是 1kHz。接下来配 ADC 和 DMA寄存器配置如下#define ECG_BUFFER_SIZE 400 // 500Hz 采样下0.8 秒一帧 uint16_t ecg_buffer[ECG_BUFFER_SIZE]; void ADC_DMA_Init(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_0; // PA0 接模拟前端输出 gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); ADC_InitTypeDef adc; adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; // 单通道不扫描 adc.ADC_ContinuousConvMode DISABLE; // 由定时器外部触发 adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T3_TRGO; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); // 心电信号源阻抗高采样时间设为最长的 239.5 周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)ecg_buffer; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize ECG_BUFFER_SIZE; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; // 循环模式采完从头覆盖 dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC | DMA_IT_HT, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }这里最关键的参数是ADC_SampleTime_239Cycles5。很多人忽略采样时间ADC 内部采样电容需要在规定时间内充到输入电压。心电信号经过模拟前端后源阻抗仍然偏高如果用默认的 1.5 周期或 7.5 周期采样电容充不满读到的数值会系统性偏低且抖动明显。239.5 周期换算一下是 3.3μs对 500Hz 的采样率绰绰有余。3.3 用 DMA 半传输中断实现双缓冲主循环不丢数据DMA 设成循环模式后它会循环往数组里写数据。问题来了如果主循环处理数组时DMA 正在往数组后半段写新数据读写就会冲突。解决办法是启用半传输中断和传输完成中断把缓冲区切成两半交替处理。400 个采样点前 200 个采满触发半传输中断后 200 个采满触发传输完成中断主循环总在处理「当前没被 DMA 写入」的那半块数据。volatile uint8_t ecg_half_flag 0; volatile uint8_t ecg_full_flag 0; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) { ecg_half_flag 1; DMA_ClearITPendingBit(DMA1_IT_HT1); } if (DMA_GetITStatus(DMA1_IT_TC1)) { ecg_full_flag 1; DMA_ClearITPendingBit(DMA1_IT_TC1); } }主循环里只在标志位为 1 且上一次处理已完成时才处理数据处理完立刻清标志。这里要注意不要在中断服务函数里做滤波或存储操作否则中断占用时间过长下一轮 DMA 触发时可能出现竞争。中断里只置标志位数据加工全部放到主循环这是嵌入式开发的通用原则。4. 数字滤波才是波形好看的关键50Hz 陷波与基线漂移抑制4.1 为什么模拟端做了滤波还要数字端再滤一遍模拟端的低通和高通只能做粗略的频带限制真正难对付的是 50Hz 工频干扰。右腿驱动能抑制大部分共模干扰但信号经过长导线传输时差模路径上仍会感应出工频分量而且人体内部的电极极化电位也会缓慢变化。模拟滤波要做到 50Hz 处极窄的陷波需要高精度元件和精心调试的电感或双 T 网络性价比很低。所以工程上普遍的做法是模拟端粗滤数字端精滤。STM32F103 主频 72MHz做几阶 IIR 滤波器完全跑得动。数字滤波器没有元件温漂问题系数定了就是定了一致性远好于模拟方案。这就是我用数字陷波处理 50Hz 的原因。4.2 50Hz 陷波器的 C 实现与 Q 值选择数字陷波滤波器我常用双二阶 IIR 结构它只保留 5 个系数和 4 个历史状态变量非常适合在单片机上跑。先看初始化和处理函数这是可以直接抄走的代码#include math.h typedef struct { float b0, b1, b2; float a1, a2; float x1, x2, y1, y2; } IIRBiquad; // 初始化 50Hz 陷波器fs 为采样率f0 为陷波频率Q 控制带宽 void notch_init(IIRBiquad *f, float fs, float f0, float Q) { float w0 2.0f * PI * f0 / fs; float alpha sinf(w0) / (2.0f * Q); float a0 1.0f alpha; f-b0 1.0f / a0; f-b1 (-2.0f * cosf(w0)) / a0; f-b2 1.0f / a0; f-a1 (-2.0f * cosf(w0)) / a0; f-a2 (1.0f - alpha) / a0; f-x1 f-x2 0.0f; f-y1 f-y2 0.0f; } // 逐点滤波x 为当前采样值返回滤波结果 float notch_process(IIRBiquad *f, float x) { float y f-b0 * x f-b1 * f-x1 f-b2 * f-x2 - f-a1 * f-y1 - f-a2 * f-y2; f-x2 f-x1; f-x1 x; f-y2 f-y1; f-y1 y; return y; }调用方式是在主循环里对每个采样点逐个处理IIRBiquad notch; notch_init(notch, 500.0f, 50.0f, 20.0f); for (int i 0; i ECG_BUFFER_SIZE; i) { ecg_buffer[i] notch_process(notch, ecg_buffer[i]); }Q 值这个参数需要认真对待。Q 值越高陷波带宽越窄对 50Hz 附近的信号伤害越小但动态响应变慢处理 QRS 波这种陡峭信号时会产生振铃Q 值太低比如小于 5陷波带宽会宽到把 40Hz 和 60Hz 附近的有效频段一起削弱T 波形态会受影响。我在 500Hz 采样率下常用的 Q 值是 1530这个范围能在压制工频和保留波形细节之间取得平衡。4.3 基线漂移怎么抑制二阶高通滤波器的边界基线漂移来自两个来源一是电极与皮肤接触界面电位缓慢变化二是呼吸引起的胸部形变让电极位置微动。漂移的频率通常在 0.050.5Hz 之间形态上表现为整条波形上下缓慢浮动。解决办法是加高通滤波但截止频率必须谨慎选择。心电信号里的 ST 段频率低至 0.67Hz 左右如果高通截止频率设到 1Hz 以上ST 段会被严重压制波形看上去像是「飘在空中」临床意义的形态判断会失真。所以基线漂移高通截止频率建议控制在 0.5Hz 以下。以下是一个二阶巴特沃斯高通的实现// 初始化高通滤波器fc 为截止频率 void highpass_init(IIRBiquad *f, float fs, float fc) { float w0 2.0f * PI * fc / fs; float alpha sinf(w0) * 0.7071f; // sqrt(2)/2巴特沃斯系数 float b0 (1.0f cosf(w0)) / 2.0f; float b1 -(1.0f cosf(w0)); float b2 b0; float a0 1.0f alpha; float a1 -2.0f * cosf(w0); float a2 1.0f - alpha; f-b0 b0 / a0; f-b1 b1 / a0; f-b2 b2 / a0; f-a1 a1 / a0; f-a2 a2 / a0; f-x1 f-x2 0.0f; f-y1 f-y2 0.0f; }我在实际测试中取 fc 0.3Hz采样率 500Hz效果是基线在 5 秒内恢复到零点附近同时 ST 段形状没有明显失真。需要提醒的是IIR 滤波器有一种固有特性叫「相位失真」对心电这种时域形态敏感的波形会有影响但 50Hz 陷波和 0.3Hz 高通带来的失真在原型验证阶段完全可以接受。如果未来要做更严谨的波形分析可以考虑用零相位滤波但那需要整帧处理不适合实时流式传输的场景。5. 心电采集的 6 个高频踩坑点从波形发飘到右腿驱动饱和5.1 波形整体发飘、ADC 读数跳变先查参考电压和采样时间现象输出波形轮廓大体像心电但毛刺明显相邻采样点之间随机跳变几十个 LSB用手靠近板子时更严重。原因有两个第一个是 ADC 参考电压。很多板子直接把 3.3V 电源接到 VREF 引脚而这个 3.3V 如果来自 USB 转串口模块或开关电源纹波可能达到几十毫伏ADC 量化时会直接把这部分纹波采进去。我踩过这个坑用示波器看 VREF 引脚上面叠加着明显的开关噪声。解决给模拟部分单独供电VDDA 和 VREF 之间加磁珠和 10μF 电容或者干脆用独立基准芯片把参考电压固定住。更省事的办法是测试时改用电池供电电源纯净度立刻上一个台阶。第二个原因是 ADC 采样时间配置太短。代码里如果用了默认采样时间ADC 内部采样开关闭合时间太短外部源阻抗又高采样电容电压还没稳定就被断开每次采的值都带随机误差。解决方法是把采样时间改到 239.5 周期配合降低源阻抗在模拟前端输出和 ADC 引脚之间加一个电压跟随器也能显著改善。5.2 50Hz 干扰压不下去右腿驱动饱和与地线环路现象滤波代码加了陷波器50Hz 依然明显波形像一条抖动的水草陷波器输出的波形虽然变平滑但 QRS 波幅度明显减小。原因在于陷波器只能处理数字化后的信号如果模拟前端在 ADC 之前就已经饱和或失真数字滤波救不回来。最常见的诱因是右腿驱动饱和电极接触不良、右腿电极脱落或人体与板子之间阻抗过大时共模反馈环路失效仪表放大器的共模抑制能力大幅下降输出端混入大量 50Hz 信号幅度可能达到几百毫伏直接顶到运放输出摆幅边界。解决先用万用表确认电极与皮肤之间的阻抗通常应该低于几十千欧。再查右腿驱动输出如果它已经接近电源轨说明反馈环路在拼命工作但没效果。我给右腿驱动输出串了一个 100kΩ 保护电阻同时在软件里加了一个导联脱落检测当信号长时间落在 ADC 满量程附近且没有心跳波形时提示用户重新贴电极。现象示波器探头接地夹夹在板子数字地上时50Hz 干扰异常增大。原因示波器探头接地线通过电源地形成一个大环路环路像天线一样接收工频磁场。这在调试时极具迷惑性因为硬件没变只是测量方式变了。解决调试时使用隔离变压器或差分探头或者把示波器地线夹到模拟地上单点连接避免环路。5.3 DMA 数据错位、波形有断裂缓冲区和中断时机不对现象串口打印波形时每隔固定间隔会出现一次跳变波形从中间断开而且断开位置每次都落在数组边界附近。原因DMA 循环模式写满整个数组后从头覆盖主循环读数据的时候DMA 已经写到了数组的前半段二者产生竞争。如果只在传输完成中断里处理数据处理期间 DMA 继续写入等处理完成时数组的前半部分已经混合了新老两批数据。解决使用半传输中断加传输完成中断的双缓冲机制第 3.3 节里的代码就是在解决这个问题。另一个容易忽视的点是滤波处理必须在一帧数据期间完成如果同一个数组既被 DMA 写又同时被滤波循环读就把滤波的结果写回另一个缓冲区处理完再整体拷贝或者通过指针切换。我见过有人在原地做滤波导致数据错乱的案例后来我在 main 函数里用两个数组来回切换问题就消失了。6. 进阶验证技巧用信号发生器给整条链路做「体检」原型系统跑起来之后别急着贴电极测人。先做一步标准测试用信号发生器输出 1mV、10Hz 的正弦波串联一个 100kΩ 电阻模拟人体源阻抗接到仪表放大器输入端。这个测试能暴露 90% 的前端问题。逐级测输出仪表放大器输出应该有约 20mV 的正弦波主放大器输出应该有约 1V 的正弦波ADC 读到约 3100 个 LSB参考 3.3V 时 1V 对应约 1241LSB经过滤波后波形应该纯净无毛刺幅度基本不变。哪一级不对就查哪一级这一步能省掉大量贴电极排查的时间。测试点输入信号预期输出实测仪表放大器输出1mV / 10Hz 正弦约 20mV 正弦若偏差超 20% 查 RG 焊接主放大器输出接前级输出约 1V 正弦若波形削顶降低增益ADC 原始值同上约 1241 LSB 波动若噪声大查参考电压数字滤波后同上波形平滑无毛刺若仍有 50Hz查陷波系数验证完链路线性度和噪声底之后我给这套系统留了三个扩展口一是把滤波后的数据降采样到 50Hz存进 SD 卡方便事后用上位机分析二是加一个 RTC 时间戳每次采集事件记录下来便于统计心率变异性三是把串口改为低功耗模式用定时器周期唤醒单片机满足便携设备的功耗要求。我每次改完模拟增益都会重新做一轮正弦扫描而不是直接贴电极。这是血泪经验换来的习惯——人身上的信号太不稳定拿它当调试依据只会把问题归咎于玄学。这套 STM32F103 心电采集系统模拟前端 20 倍加 50 倍增益500Hz 采样率50Hz 陷波加 0.3Hz 高通是一套能跑通、能出图、能让你理解每个环节的最小闭环。在这个基础上加存储、加无线、加算法都不会走弯路。希望帮到你。本文还有配套的精品资源点击获取