
1. 心电采集仪的整体方案设计思路1.1 为什么选择STM32作为主控做心电采集这个方向主控选型其实没有太多悬念。心电信号的采样率要求通常在250Hz到1kHz之间加上还要做数字滤波、心率计算、波形显示或者串口上传对MCU的运算能力和外设资源有一定要求。STM32F103系列在这类生物电信号采集项目里出现频率极高原因很直接Cortex-M3内核跑72MHz自带12位ADC多路定时器和DMA串口、SPI、I2C一应俱全关键是资料多、价格低、开发工具链成熟。我实际对比过几种方案。用51单片机也能做心电采集但ADC外置、RAM太小、做不了实时滤波波形出来毛刺多得没法看。用ESP32的话WiFi功能是加分项但模拟部分的电源噪声控制会更麻烦而且ADC线性度在精密采集场景下不如STM32稳定。所以STM32F103C8T6这个最小系统板就成了性价比最高的选择几十块钱就能搞定核心板。1.2 模拟前端的核心架构心电信号的本质是什么体表电极采集到的差分电压信号幅度在0.5mV到4mV之间频率主要集中在0.05Hz到100Hz。这个信号有两个特点让电路设计变得棘手一是极其微弱二是叠加了大量共模干扰主要是50Hz工频干扰。所以模拟前端需要完成三件事差分放大、共模抑制、滤波。典型架构是三级处理第一级用仪表放大器做差分放大和共模抑制第二级用运放做高通滤波去掉直流偏置第三级做低通滤波限制带宽并进一步放大。这里就涉及到AD620和OP07的配合。AD620是经典的仪表放大器增益用一个外部电阻就能设定共模抑制比CMRR在G100时能达到100dB以上输入阻抗高噪声低非常适合做第一级。OP07则是精密运放失调电压极低典型值10μV温漂小适合做后续的滤波和放大级。这两个器件的组合在低成本心电采集方案里非常经典。1.3 整体信号链规划从电极到STM32的ADC输入完整的信号链是这样的体表电极三导联RA、LA、RL采集原始心电信号进入AD620差分放大级增益设为约100倍经过OP07构成的高通滤波器截止频率约0.05Hz去除电极极化产生的直流偏置再经过OP07构成的低通滤波器截止频率约100Hz滤除高频噪声可选加一级50Hz陷波器抑制工频干扰信号送入STM32的ADC引脚进行采样STM32内部做数字滤波、QRS检测、心率计算通过OLED或串口输出结果这个链路看起来简单但每一级的参数选择都有讲究后面会逐一拆解。2. 模拟前端电路的深度解析与参数计算2.1 AD620差分放大级的设计要点AD620的增益公式是G 1 49.4kΩ / Rg其中Rg是接在1脚和8脚之间的增益电阻。要得到G100计算一下Rg 49.4kΩ / (100-1) ≈ 499Ω。实际选标准值499Ω或者510Ω的金属膜电阻精度至少1%温漂越小越好。这里有个容易踩的坑很多人直接把AD620的增益设到1000倍觉得放大越多越好。实际上第一级增益太高会导致饱和——电极刚接触皮肤时会有较大的瞬态偏置放大1000倍直接撞轨。而且AD620在增益很高时带宽会下降G1000时带宽只剩几十kHz虽然对心电信号够用但噪声增益也会同步放大。我的经验是G100左右最稳妥后续再用OP07补增益。AD620的5脚是REF引脚决定了输出信号的参考电平。在单电源供电场景下这个引脚不能接地要接到一个中间电平比如VCC/2否则负半周信号会被削掉。我一般用两个等值电阻分压加一个跟随器来产生这个参考电压保证低阻抗输出。注意AD620的电源去耦非常关键。每个电源引脚旁边必须放0.1μF陶瓷电容和10μF钽电容且尽量靠近芯片引脚。我在第一版PCB上忽略了这点结果波形上叠加了明显的开关噪声排查了半天才发现是去耦不足。2.2 OP07高通滤波器的参数选择高通滤波器的目的是去掉直流偏置。心电信号的最低频率成分大约在0.05Hz所以高通截止频率要低于这个值通常设在0.03Hz到0.05Hz之间。用OP07搭一阶有源高通滤波器截止频率公式是fc 1 / (2πRC)。取fc0.05Hz如果C10μF则R 1/(2π×0.05×10×10^-6) ≈ 318kΩ。这个电阻值比较大要注意OP07的输入偏置电流在电阻上产生的额外失调。OP07的输入偏置电流典型值是±2nA在318kΩ上产生的压降约0.6mV相对于心电信号幅度来说可以接受但如果用更高阻值就要小心了。电容的选择也有讲究。10μF的电容如果用陶瓷电容容值随电压和温度变化较大而且有压电效应振动会产生噪声。我推荐用钽电容或者薄膜电容稳定性好得多。之前用0805封装的X7R陶瓷电容做这个位置结果用手碰一下板子波形就抖换成钽电容后问题消失。2.3 OP07低通滤波器的实现低通滤波器的作用是限制带宽滤除100Hz以上的高频噪声。截止频率设在100Hz到150Hz之间比较合适既能保留心电信号的主要成分又能有效抑制高频干扰。同样用OP07搭一阶有源低通fc 1/(2πRC)。取fc100Hz如果R100kΩ则C 1/(2π×100×100×10^3) ≈ 16nF。选标准值15nF或18nF都可以。一阶滤波器的滚降斜率是-20dB/十倍频程对高频噪声的抑制能力有限。如果对波形质量要求高可以用二阶Sallen-Key结构滚降变成-40dB/十倍频程。但二阶电路对元件匹配要求更高调试也更麻烦。我的建议是先用一阶跑通确认整体链路没问题后再考虑升级。2.4 50Hz工频陷波器的取舍工频干扰是心电采集最大的敌人。虽然AD620的共模抑制比很高但电极不平衡、导线走线不对称等因素会把共模干扰转化成差模信号最终出现在输出端。陷波器可以有效抑制50Hz干扰但它也有副作用陷波器在50Hz附近引入相位失真而心电信号的QRS波群频谱成分恰好覆盖这个频段可能导致波形畸变。而且陷波器的Q值越高对元件精度的要求就越苛刻稍微偏差一点陷波频率就偏了。我的实际做法是先不加陷波器通过良好的电极接触、屏蔽导线、合理的PCB布局来降低工频干扰。如果实测下来50Hz干扰仍然明显再考虑加陷波器。加的话用双T网络配合OP07做有源陷波Q值不要设太高3到5左右比较合适。3. STM32端的软件设计与实操过程3.1 开发环境搭建与工程配置STM32的开发环境选择上Keil MDK和STM32CubeIDE是主流。Keil的优势是编译速度快、调试方便但需要手动安装芯片包。CubeIDE免费且集成了CubeMX配置工具生成初始化代码很方便。如果用Keil安装完MDK后需要下载STM32F1系列的Device Family Pack。这一步经常有人卡住——打开Keil后找不到STM32的器件选项就是因为芯片包没装。下载好.pack文件后双击安装即可。另外注意Keil5默认不兼容C51如果之前装过Keil4做51开发两者可以共存但需要分别安装在不同目录。新建工程时我习惯用标准库而不是HAL库。标准库的代码量小、执行效率高对F103这种资源有限的芯片更友好。工程目录结构一般是Project/ ├── CMSIS/ # 内核相关文件 ├── FWLIB/ # 标准外设库 ├── USER/ # 用户代码 │ ├── main.c │ ├── stm32f10x_it.c │ └── ... ├── HARDWARE/ # 硬件驱动 │ ├── adc.c │ ├── oled.c │ └── ... └── SYSTEM/ # 系统文件 ├── delay.c ├── usart.c └── ...3.2 ADC采样与DMA传输配置心电采集对ADC的要求是采样率稳定、不丢点、噪声低。STM32F103的ADC最高采样率是1MSPS12位但实际用的时候不需要那么快。心电信号带宽100Hz按照奈奎斯特采样定理采样率至少200Hz实际取250Hz到500Hz就够了。我用定时器触发ADC采样这样采样率最稳定。具体配置TIM2设置为PWM模式频率设为500Hz用TIM2的更新事件触发ADC规则通道转换。ADC配置为连续转换模式开启DMA传输把转换结果自动搬到内存数组里。这样CPU完全不用干预采样过程只需要在DMA传输完成中断里处理数据。ADC的参考电压直接用VDDA所以模拟部分的电源质量直接影响采集精度。建议VDDA和VSSA单独走线在靠近芯片的地方加磁珠和电容滤波。如果对精度要求高可以用外部基准电压芯片如REF3033提供3.3V基准。// ADC初始化关键代码 void ADC_Init_Config(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // DMA配置 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (u32)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)adc_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // ADC配置 ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC2; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // ADC校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }3.3 数字滤波与QRS检测算法ADC采到的原始数据噪声很大必须做数字滤波。我一般用两级滤波先做一个IIR低通滤波器去掉高频噪声再用一个滑动平均滤波器进一步平滑波形。IIR低通滤波器的差分方程是y[n] α × x[n] (1-α) × y[n-1]其中α dt / (RC dt)dt是采样周期RC是时间常数。采样率500Hz时dt2ms如果截止频率取40HzRC 1/(2π×40) ≈ 4ms则α 2/(42) ≈ 0.33。QRS检测是心率计算的核心。我用的是一种简化的阈值法先对滤波后的信号做差分平方运算增强QRS波群的斜率特征再用移动窗口积分得到包络信号最后用自适应阈值判断QRS波群的位置。具体步骤对滤波后的信号做五点差分d[n] x[n2] - x[n-2]平方s[n] d[n] × d[n]移动窗口积分窗口宽度约150ms即75个采样点y[n] (1/N) × Σs[i]自适应阈值threshold 0.5 × max(y[n-100:n])当y[n]超过阈值且在一定不应期内200ms判定为一次QRS波群心率计算公式HR 60 / RR间期秒。为了稳定显示我一般取最近8个RR间期的平均值来计算心率。3.4 OLED显示与串口数据上传显示部分用0.96寸OLEDSSD1306驱动I2C接口就够了。屏幕上分两行显示第一行显示实时心率值第二行画一个简易的心电波形。波形绘制就是把最近128个采样点映射到屏幕的Y轴范围用画点函数逐点绘制。串口上传是为了在电脑上做进一步分析。STM32的USART1配置为115200波特率8位数据位1位停止位无校验。每采完一帧数据比如256个点通过串口发送一个数据包格式为帧头0xAA 0x55 数据长度 数据内容 校验和。上位机用Python或MATLAB接收并实时绘图。// 串口发送数据包 void Send_ECG_Frame(u16 *data, u16 len) { u8 header[2] {0xAA, 0x55}; u8 checksum 0; USART_SendData(USART1, header[0]); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, header[1]); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); for(u16 i 0; i len; i) { u8 high (data[i] 8) 0xFF; u8 low data[i] 0xFF; USART_SendData(USART1, high); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, low); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); checksum high low; } USART_SendData(USART1, checksum); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); }4. 常见问题排查与实操避坑指南4.1 波形噪声大、基线漂移严重这是心电采集最常遇到的问题。波形上叠加高频毛刺或者基线像波浪一样上下漂移。排查思路要按信号链逐级定位。先看电源。用示波器交流耦合档测模拟电源轨上的纹波如果纹波超过几mV说明电源滤波不够。解决办法是在每个运放电源引脚旁加0.1μF陶瓷电容和10μF钽电容模拟部分和数字部分用磁珠隔离供电。再看电极。电极接触不良是基线漂移的头号原因。一次性心电电极的导电膏干了之后接触阻抗急剧上升导致信号漂移。建议用质量好一点的电极贴之前用酒精棉擦拭皮肤去除油脂。如果只是做实验可以用镀金电极片配合导电膏。最后看PCB布局。模拟走线和数字走线要分开不能平行走线。AD620的输入引脚走线要尽量短且对称减少共模干扰转化为差模信号的机会。地平面要完整模拟地和数字地单点连接。4.2 ADC采样值跳动大如果ADC读出来的值一直在跳先确认参考电压是否稳定。用万用表测VDDA引脚如果电压波动超过几mV采集精度肯定受影响。可以在VDDA引脚加一个100μF电解电容和一个0.1μF陶瓷电容。另一个原因是ADC采样时间设置太短。STM32的ADC输入阻抗和采样电容需要足够的充电时间。如果信号源阻抗较高比如经过大电阻分压后采样时间要设长一些。我一般用55.5个周期的采样时间对应约1μs的采样窗口对大多数场景够用。还有一点容易忽略ADC的输入电压范围是0到VDDA如果模拟前端输出的信号超出了这个范围ADC会削顶。所以要确保AD620的REF引脚电压设置正确使输出信号始终在0到3.3V之间。4.3 心率计算不准或跳变心率跳变通常是因为QRS检测算法把噪声误判为QRS波群或者漏检了真实的QRS波群。改进方向有几个调整差分窗口大小。窗口太窄对噪声敏感太宽会漏掉窄QRS波。一般取采样率的10%到20%比较合适。增加不应期。两次QRS波群之间的最小间隔设为200ms防止T波被误判。用双阈值检测。高阈值确保不漏检低阈值配合搜索回溯机制防止漏检。对RR间期做中值滤波。连续8个RR间期去掉最大最小值后取平均能有效抑制偶发的误检。我在实际调试中发现如果波形质量本身不好再怎么优化算法也是事倍功半。所以优先把模拟前端的信号质量做好算法只是锦上添花。4.4 常见问题速查表问题现象可能原因排查方法解决方案波形全是噪声电极未接好或脱落检查电极接触重新贴电极用导电膏基线漂移严重电极极化或接触阻抗变化观察漂移频率更换电极加高通滤波50Hz干扰明显电源干扰或走线不对称示波器看频谱加陷波器改善屏蔽ADC值跳动大参考电压不稳或采样时间短测VDDA纹波加去耦电容增加采样时间心率跳变QRS误检或漏检打印RR间期优化阈值和不应期串口数据乱码波特率不匹配检查两端配置统一波特率设置OLED不显示I2C地址错误或接线问题用逻辑分析仪抓I2C确认地址0x78或0x7A4.5 几个容易被忽略的实操细节电极导线的屏蔽从电极到AD620输入端的导线一定要用屏蔽线屏蔽层接模拟地。普通杜邦线做心电采集工频干扰会大到没法看。我试过用屏蔽线和不屏蔽线的对比效果差距非常明显。AD620的输入保护人体可能带静电直接接触电极时瞬间高压可能损坏AD620。在输入端串联一个10kΩ电阻再并联两个反向的二极管到电源和地可以起到保护作用。虽然AD620内部有一定保护但外部再加一层更保险。STM32的JTAG引脚复用STM32F103的PA15、PB3、PB4默认是JTAG功能如果要用作普通GPIO需要禁用JTAG。但禁用后就不能用JTAG调试了只能用SWD。所以建议一开始就用SWD模式保留PA15、PB3、PB4作为普通IO使用。延时函数卡死如果用SysTick做延时在中断里调用延时函数会导致卡死。因为SysTick中断优先级如果低于当前中断中断嵌套时SysTick无法触发延时函数就死循环了。解决办法是在中断里不要用延时或者用其他定时器做延时。电源上电顺序模拟部分和数字部分如果分别供电要注意上电顺序。先给数字部分上电再给模拟部分上电或者同时上电。如果模拟部分先上电运放输出可能通过保护二极管向数字部分漏电流导致异常。5. 从原型到成品的优化方向5.1 硬件层面的改进空间原型阶段用模块搭出来的电路做到成品需要考虑更多。PCB布局上模拟部分和数字部分要分区布局AD620和OP07尽量靠近电极接口减少输入走线长度。电源部分用LDO单独给模拟供电比如用AMS1117-3.3给数字部分再用一个低噪声LDO给模拟部分。如果要做便携式锂电池供电是必然选择。但锂电池的电压范围是3.0V到4.2V需要升降压转换。这里可以用STM32配合Buck-Boost电路实现不过要注意开关电源的纹波对模拟电路的干扰。我的做法是开关电源先升到5V再用LDO降到3.3V给模拟部分这样纹波能压到很低。5.2 软件层面的功能扩展基础版本跑通后可以加不少实用功能。比如用STM32的内部RTC加时间戳把心电数据存储到SD卡里方便后续回放分析。或者加一个蓝牙模块把数据无线传到手机APP上显示。如果要做心律失常的简单分类可以在STM32上跑一个轻量级的神经网络。不过F103的RAM只有20KB跑神经网络比较吃力可以考虑换F4系列。或者把特征提取放在STM32上做把特征值传到上位机做分类。5.3 标定与验证方法心电采集仪做出来之后怎么验证它准不准最直接的方法是用标准信号源输入已知幅度和频率的正弦波看采集出来的波形幅度和频率是否一致。比如输入1mV峰峰值的100Hz正弦波增益设为1000倍ADC读到的值应该在1V左右对应ADC值约1240。另一个方法是用商用心电模拟器它能输出标准的心电波形包括各种心律失常模式。对比采集波形和标准波形可以评估系统的保真度。如果没有这些设备至少要和商用心电监测设备做同步对比看心率读数是否一致。我在实际测试中发现系统在静态下的噪声水平大约在20μV峰峰值左右对于心电信号典型1mV来说信噪比约34dB足够看清P波、QRS波群和T波。如果要看清更微弱的信号细节比如心房颤动时的f波还需要进一步降低噪声这就对电源和布局提出了更高要求。最后分享一个调试小技巧在AD620的输出端接一个测试点用示波器直接观察模拟前端的输出波形。这样可以把问题定位在模拟部分还是数字部分。如果模拟输出干净但ADC读出来脏问题在ADC配置或电源如果模拟输出本身就脏问题在前端电路。这个分界点能帮你省下大量排查时间。