1. 项目缘起与整体设计思路
心电信号采集这个方向,在生物医学电子里算是经典中的经典。我最早接触它是在做一个便携式健康监测终端的时候,当时市面上能买到的成品心电模块要么贵得离谱,要么噪声大得没法看,索性自己从运放和单片机开始搭一套。这套基于STM32的心电采集仪,核心目标很明确:把人体体表那点几十微伏到几毫伏的心电信号,干净地放大、滤波、数字化,然后通过串口或者屏幕把波形实时呈现出来。它适合电子类专业的学生做毕业设计,也适合想入门生物电信号采集的硬件工程师拿来练手。
先说清楚这套系统到底在干什么。人体心电信号有几个非常“难伺候”的特点:幅度极小,典型值只有1mV左右,最微弱的有用成分可能只有几十微伏;频率很低,主要能量集中在0.05Hz到100Hz之间;而且它天然带着一大堆干扰——50Hz工频干扰、电极接触噪声、人体自身的肌电噪声、还有直流极化电压。你要做的,就是在一堆噪声里把那个微弱的有用信号“捞”出来,还不能把它弄变形。这就决定了整个设计的主线:前级高共模抑制比放大 + 中间级高通滤波去基线漂移 + 低通滤波去高频噪声 + 后级再放大到ADC能识别的范围。
为什么选STM32而不是51或者别的MCU?原因很实际。心电信号采样率一般要做到250Hz到1kHz,12位分辨率是底线,STM32F103系列自带12位ADC,转换速率1MHz,配合DMA搬运完全不占CPU。而且STM32的定时器可以精确控制采样周期,这对心电这种对时间轴要求严格的信号来说太重要了。另外STM32的串口、SPI、I2C资源丰富,后面想加OLED屏、SD卡存储、蓝牙传输都很方便。相比之下,51单片机做心电采集,光是ADC外挂和时序控制就够你喝一壶的。
模拟前端这块,AD620和OP07是绕不开的两个器件。AD620是经典的三运放仪表放大器,增益用一个电阻就能设定,共模抑制比高,输入阻抗大,特别适合做心电信号的第一级。OP07则是低失调电压的精密运放,虽然带宽不高,但做滤波和电平抬升非常稳。这两个器件搭配起来,成本可控,性能也够用。当然有人会问AD820怎么样,AD820是单运放,输入偏置电流更低,但做仪表放大需要自己搭三运放结构,匹配电阻要求高,对新手不太友好。AD620集成度高,省事,这是选它的核心理由。
整个系统的信号链路我理一下:电极从人体采集信号,经过AD620做第一级差分放大,增益设在100倍左右;然后过一个由OP07构成的高通滤波器,截止频率0.05Hz,把电极极化产生的直流漂移压掉;接着再过一个低通滤波器,截止频率100Hz到150Hz,把高频噪声和工频干扰的谐波滤掉;之后再用一级OP07做同相放大,把信号抬到0到3.3V范围内,同时加一个直流偏置,让信号落在ADC量程中间;最后送进STM32的ADC引脚,由定时器触发采样,DMA搬运到内存,再做数字滤波和波形显示。
这个链路里每一步都有讲究,后面我会逐个拆开讲。先记住一个原则:模拟部分决定信号质量的上限,数字部分只是尽量不把这个上限拉低。很多人把精力全花在STM32的代码上,结果模拟前端一塌糊涂,采出来的波形全是噪声,那就本末倒置了。
2. 模拟前端核心细节与实操要点
2.1 AD620第一级放大电路的设计与计算
AD620的增益公式是G = 1 + 49.4kΩ / Rg,其中Rg是接在1脚和8脚之间的增益电阻。我要的增益是100倍,代入公式:100 = 1 + 49400 / Rg,解得Rg = 49400 / 99 ≈ 499Ω。实际选型时用500Ω的金属膜电阻,精度1%就够了,算下来增益是99.8倍,完全可接受。这里有个细节:Rg的精度直接影响增益精度,但心电采集对绝对增益精度要求没那么高,1%足够了,没必要上0.1%的贵电阻。
AD620的输入端接法要注意。人体心电信号是差分信号,左右手电极分别接IN+和IN-,右腿驱动电极单独处理。这里必须提右腿驱动电路,它是抑制共模干扰的关键。原理很简单:从AD620的两个输入端各取一个电阻,通常各1MΩ,汇合后经过一个反相放大器反馈到右腿电极。这样人体上的共模电压被反相放大后驱动回人体,形成负反馈,共模干扰被大幅压制。没有右腿驱动,50Hz工频干扰能大到把心电信号完全淹没。我实测过,不加右腿驱动时,示波器上看输出全是50Hz正弦,加了之后心电波形立刻清晰。
AD620的电源用±5V双电源供电,这样输入共模范围更宽,输出动态范围也大。电源引脚各接一个0.1μF陶瓷电容和10μF钽电容去耦,位置尽量靠近芯片引脚。参考端5脚接地,如果后面要做电平抬升,也可以在这里加偏置电压,但建议第一级参考端直接接地,保持差分纯净。
注意:AD620的输入偏置电流约1nA,输入端如果直接接电极,电极阻抗不平衡会产生额外失调。实际使用时,输入端对地各接一个10MΩ电阻提供偏置电流回路,这个电阻要匹配,误差控制在1%以内。
2.2 OP07高通滤波与低通滤波的配合
第一级放大之后,信号里混着电极极化产生的直流电压,可能有好几百毫伏,如果不滤掉,后面再放大就饱和了。高通滤波器用OP07搭一个一阶有源高通,截止频率0.05Hz。计算RC:f = 1 / (2πRC),取C = 1μF,则R = 1 / (2π × 0.05 × 1e-6) ≈ 3.18MΩ。实际用3.3MΩ电阻和1μF电容,截止频率约0.048Hz,够用了。这里电容要选漏电流小的钽电容或者薄膜电容,电解电容漏电流大会导致输出漂移。
低通滤波器同样用OP07,截止频率设150Hz,二阶压控电压源结构,这样滚降更陡。二阶低通需要两个电阻两个电容,计算稍微麻烦点。我一般取C1 = C2 = 0.1μF,然后根据f = 1 / (2πRC)反推R。150Hz对应R ≈ 1 / (2π × 150 × 0.1e-6) ≈ 10.6kΩ,取10kΩ标准值,实际截止频率约159Hz。二阶滤波器的Q值设0.707,这样通带最平坦。反馈电阻根据Q值算,具体公式网上都有,这里不展开,记住Q=0.707对应反馈电阻比输入电阻约0.586倍就行。
为什么高通和低通要分开做,不用一个带通?分开做的好处是每一级可以独立调整,调试时先调高通看基线稳不稳,再调低通看噪声大不大,互不干扰。而且OP07做滤波时,每一级增益可以设成1,避免增益带宽积不够导致高频衰减。OP07的增益带宽积只有0.6MHz,如果一级又滤波又放大,很容易自激。
2.3 后级电平抬升与ADC驱动
经过前三级之后,信号幅度大概在1V到2V峰峰值,但它是双极性的,有正有负。STM32的ADC只能测0到3.3V,所以必须把信号抬到1.65V附近。用OP07搭一个同相加法器,一路输入是滤波后的心电信号,另一路是1.65V的基准电压,两路相加后输出。基准电压用TL431或者电阻分压加电压跟随器产生,分压电阻取10kΩ和10kΩ,从3.3V分压得到1.65V,再用OP07做电压跟随器降低输出阻抗。
后级放大倍数设2倍左右,这样1mV的心电信号经过前级100倍变成100mV,再经过后级2倍变成200mV,落在ADC量程内。ADC的参考电压用STM32内部的3.3V,12位分辨率下,1LSB = 3.3V / 4096 ≈ 0.8mV。200mV的信号对应250个LSB,分辨率足够看清心电波形的主要特征。如果想更精细,可以把后级增益提到5倍,但要注意输出不能超过3.3V,否则削顶。
ADC输入端加一个RC低通,截止频率设1kHz左右,R取1kΩ,C取0.1μF,滤掉高频开关噪声。这个RC还有另一个作用:STM32的ADC采样时内部采样电容会瞬间抽取电流,这个RC可以缓冲,避免信号被拉低。采样时间设55.5个ADC周期,保证采样电容充分充电。
实操心得:OP07做电压跟随器时,输出端一定要串一个几十欧姆的电阻再接到ADC,防止容性负载导致振荡。我一开始没加,波形上总是有高频毛刺,加了33Ω电阻后干净了。
3. STM32数字部分与系统联调
3.1 定时器触发ADC与DMA搬运配置
STM32F103的ADC1接在PA0引脚,对应通道0。配置步骤我按实际工程顺序说。先开时钟:RCC_APB2ENR使能ADC1、GPIOA、AFIO;RCC_APB1ENR使能DMA1。然后配置PA0为模拟输入,GPIO_Mode_AIN。ADC配置:独立模式,扫描模式关闭,连续转换关闭,外部触发选择定时器2的TRGO事件,数据对齐右对齐,通道数1。定时器2配置成向上计数,预分频和重装载值决定采样率。我要的采样率是500Hz,系统时钟72MHz,预分频设72-1,得到1MHz计数频率,重装载值设2000-1,这样更新频率就是1MHz / 2000 = 500Hz。TRGO输出选择更新事件。
DMA配置:通道1,外设地址是ADC1的DR寄存器,内存地址是一个uint16_t数组,方向外设到内存,循环模式,外设和内存数据宽度都是半字,内存增量使能。缓冲区大小设500,这样每1秒DMA传输完成中断一次,在中断里处理数据。
这里有个坑:STM32F103的ADC和DMA配合时,如果DMA没使能就启动ADC,会丢数据。正确顺序是先配置DMA并使能,再使能ADC,最后启动定时器。我踩过这个坑,波形上总是隔一段丢一个点,查了半天才发现是顺序问题。
3.2 数字滤波与波形显示
采到的原始数据里还有残余噪声,数字滤波做两级:一级50Hz陷波,一级16点滑动平均。50Hz陷波用二阶IIR,系数用MATLAB或者Python算好,直接写成C数组。滑动平均简单,开一个16长度的环形缓冲区,每次新数据进来替换最老的,求和除以16。滑动平均的截止频率大概在采样率的1/16左右,500Hz采样对应约31Hz,对心电信号的主要成分影响不大,但能把高频噪声压得很低。
波形显示用0.96寸OLED,SSD1306驱动,I2C接口。显示时把500个点的缓冲区映射到128列屏幕上,每列取4个点的平均值,纵轴根据信号幅度自适应缩放。刷新率控制在20帧每秒,太高了OLED响应不过来。显示内容除了波形,还可以叠加心率数值,心率检测用简单的峰值检测算法:找滑动窗口内的最大值,如果超过阈值且距离上一个峰值超过300ms,就算一次心跳。
串口输出也保留,波特率115200,每采样点发两个字节,上位机用Python的matplotlib实时画图。这样调试时看波形更方便,OLED只是便携显示。
3.3 系统联调与噪声排查
联调时先不接人体,把输入端短接,看输出噪声。理想情况下短接时输出应该在1.65V附近,波动不超过几个LSB。如果波动大,先查电源纹波,用示波器AC耦合看±5V和3.3V,纹波应该在10mV以内。电源纹波大,多半是去耦电容没放够或者地线走线太长。
然后接信号发生器,输入1mV峰峰值、10Hz正弦波,看输出是否干净放大。如果波形失真,查后级放大是否饱和。如果噪声大,查AD620的Rg电阻是否靠近芯片,走线是否对称。我遇到过Rg电阻走线一长一短,导致共模抑制比下降,50Hz干扰明显,后来把两个电阻对称放在芯片两侧就好了。
最后接人体,用一次性心电电极,左右手和右腿。接上后先看基线,如果基线漂移大,查高通滤波的电容是否漏电。如果波形上有规律的尖峰,那是工频干扰,查右腿驱动是否正常工作。正常的心电波形应该能看到明显的P波、QRS波群和T波,QRS波群幅度最大,R波尖锐。
常见问题速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 输出饱和到电源轨 | 前级增益过大或直流漂移未滤除 | 检查高通滤波电容,测量各级输出直流电平 |
| 波形全是50Hz正弦 | 右腿驱动未工作或共模抑制比低 | 检查右腿驱动反馈回路,确认Rg电阻匹配 |
| 波形有高频毛刺 | ADC驱动RC未加或OP07振荡 | 在OP07输出串33Ω电阻,ADC输入加RC |
| 采样点丢失 | DMA和ADC使能顺序错误 | 先使能DMA,再使能ADC,最后启动定时器 |
| 基线缓慢漂移 | 高通截止频率过高或电极极化 | 降低高通截止频率,更换新电极 |
4. 常见问题与排查技巧实录
4.1 电源与地线布局的坑
模拟部分和数字部分共用一组电源时,数字开关噪声会串到模拟电源上,心电波形上会出现与STM32主频相关的周期性噪声。解决办法是模拟部分用独立的LDO供电,比如从5V经AMS1117-3.3给数字部分,再从另一路AMS1117-5.0或者直接±5V给模拟部分。如果只有一组电源,至少在电源入口处用磁珠或者电感隔离,模拟和数字地单点连接。
地线布局上,模拟地和数字地要分开走,最后在ADC下方单点汇合。我见过有人把模拟地和数字地混在一起走,结果噪声大得没法用。PCB布局时,AD620和OP07的地线要尽量短而粗,去耦电容的地引脚直接打过孔到地平面。
4.2 电极与人体接触的注意事项
心电电极接触不良是新手最容易忽略的问题。一次性电极的导电胶干了之后接触阻抗急剧上升,信号幅度下降,噪声增大。测试时如果发现波形幅度忽大忽小,先换新电极。电极贴的位置也有讲究,左右手电极贴在手腕内侧,右腿电极贴在右脚踝内侧,贴之前用酒精棉擦拭皮肤去除油脂和死皮,接触阻抗能降一个数量级。
还有一点,人体本身就是一个天线,会拾取空间中的电磁干扰。测试时远离电脑电源、显示器、日光灯,这些设备工频辐射很强。我一般在电池供电下测试,整个系统用9V电池经稳压供电,完全浮地,干扰最小。
4.3 STM32开发环境与烧录问题
Keil5安装STM32芯片包是第一步,很多人卡在这里。芯片包从Keil官网下载,双击安装后Keil会自动识别。如果Keil5里没有STM32库,检查Pack Installer里是否安装了对应的Device Family Pack。标准库新建工程时,要把启动文件、系统文件、外设驱动文件都加进去,头文件路径也要配好。我习惯用STM32CubeMX生成初始化代码,省去手动配置寄存器的麻烦,但ADC和DMA的细节还是自己写更可控。
烧录用ST-Link,接线是SWDIO、SWCLK、GND、3.3V四根线。如果ST-Link Utility识别不到芯片,先检查接线,再检查芯片是否被禁用JTAG。STM32F103的PA13和PA14是SWD引脚,如果程序里把这两个引脚配成普通GPIO,下次就烧不进去了。解决办法是按住复位键,点击烧录,松开复位键,趁芯片还没运行用户程序时抢烧。或者用BOOT0拉高进入系统存储器启动模式,用串口烧录。
独家避坑:STM32的ADC在采样时如果引脚悬空,读出来的值是随机的。调试时如果发现ADC值乱跳,先确认引脚是否接了信号。另外,ADC的参考电压如果直接用VDDA,而VDDA和VDD之间有压差,读数会不准。最好在VDDA和VSSA之间加一个1μF加0.1μF的去耦,并且VDDA通过磁珠从VDD取电。
4.4 心电信号质量的主观评价
采到波形之后怎么判断好坏?我一般看三点。第一,基线稳不稳,好的波形基线应该是一条直线,漂移不超过0.5mV。第二,QRS波群是否清晰,R波尖锐,Q波和S波对称,幅度在0.5mV到2mV之间。第三,P波和T波是否可见,P波在QRS之前,幅度小但圆滑,T波在QRS之后,幅度较大且平缓。如果P波和T波完全看不到,说明低通截止频率太低或者增益不够。
还有一个简单办法:用手捏住电极,人为制造一个干扰,看系统恢复稳定的速度。好的系统在干扰消失后一两秒内基线就回正,差的系统要好几秒甚至一直漂。这反映了高通滤波器的响应速度,截止频率越低,恢复越慢,但基线越稳,需要折中。
5. 后续扩展与个人体会
这套心电采集仪的基础版做完之后,可以扩展的方向很多。加一个SD卡模块,把原始数据存成CSV文件,方便后期用MATLAB做HRV分析。加一个蓝牙模块,把波形传到手机APP上显示,做成便携式。还可以把单导联扩展成三导联,用三片AD620分别采集I、II、III导联,STM32的ADC多通道扫描模式轮流采样。甚至可以把STM32换成带浮点运算的F4系列,在片内做实时FFT,分析心电频谱。
我个人在实际操作中的体会是,模拟前端的调试时间要占总时间的七成以上。很多人急着写代码,结果模拟部分没调好,代码写得再漂亮也采不到干净信号。我的习惯是先用示波器把每一级输出都看一遍,确认每一级的波形都符合预期,再接ADC。示波器看模拟波形,串口看数字波形,两者对照,问题出在模拟还是数字一目了然。
最后分享一个小技巧:AD620的增益电阻Rg可以用一个精密电位器代替,调试时先用电位器调到一个合适的增益,测出阻值后再换成固定电阻。这样比反复焊电阻方便得多。电位器要用多圈精密型的,单圈电位器分辨率不够,调不准。