☰
STM32F103+ADS1292心电采集与心率实时监测系统设计
2026/10/1 14:43:05 网站建设 项目流程

简介:这套工程面向使用STM32F103和ADS1292进行生理信号采集的开发者,解决心率实时显示与心电图波形绘制的需求。系统基于C语言完成,提供完整的嵌入式工程源码,能够从ADS1292读取心电数据,并在串口助手中直观呈现心率数值与波形变化,涉及R波识别、R-R间期计算、UART通信等关键算法。压缩包内共207个文件,以C源码、头文件及Keil工程配置为主,也包含编译生成的下载镜像、调试映射文件、链接脚本和备份配置,整体大小约6.39MB,目录划分清楚。已有7911人学习,工程内附详细注释和分层模块,方便理解从传感器采集、数据处理到串口传输的完整链路。对于希望掌握STM32外设驱动、串口协议或生物信号处理方法的读者,这是一份可直接运行和二次开发的参考工程,可进一步扩展为便携式健康监测设备。

1. 项目整体思路与方案选型

1.1 这个项目到底在做什么

标题里几个关键词其实已经把整件事说透了:STM32F103负责控制和数据搬运,ADS1292负责把心电模拟信号变成数字信号,串口助手负责把结果可视化。最终在电脑屏幕上能看到心电波形,同时实时显示心率数值。

这个项目非常适合两类人:一类是做嵌入式或者医疗电子方向的学生,想把手头的STM32知识延伸到生物电采集领域;另一类是工程师做可穿戴产品预研,需要快速验证心电采集方案。它不是实验室里精密仪器级别的方案,而是能在桌面上跑起来、能看波形、能算心率的最小可行系统。

我实际做完之后的感觉是:难度不在STM32端,也不在串口协议,而在ADS1292的配置和心电信号的质量处理。芯片的寄存器看着不多,但每个位都牵扯到采样率、增益、输入通路选择,没吃透手册会走很多弯路。

1.2 为什么选ADS1292而不是其他方案

做心电采集,市面上绕不开几个选择:ADI的AD8232、TI的ADS1292、以及直接用运放搭分立前端。

AD8232是模拟输出,后面接STM32的ADC,方案简单、上手快,但它把滤波、导联脱落检测都做在了芯片内部,灵活性差。而且STM32F103内部ADC的有效位数在实际测量中大概只有10-11位,心电信号幅值又只有0.5-4mV,叠加在约100mV的电极偏置电压上,小信号很容易被量化噪声吃掉。

ADS1292是24位的Delta-Sigma ADC,内置可编程增益放大器(PGA)、右腿驱动(RLD)电路,连Wilson中心端都有了,等于把传统心电前端一大半硬件都集成进去了。SPI数字接口直接给STM32,中间不需要额外的模拟调理链。实际测试下来,噪声水平远低于STM32内部ADC方案。

简单说,ADS1292把“模拟信号调理+ADC转换”这一整块变成了一个黑盒子,你只需要管好SPI读写,就能拿到干净的数字心电数据。这个选型决定了整个项目的靠谱程度。

1.3 为什么用STM32F103作为主控

原因很现实:它普及度高、资料多、成本低。最小系统板十几块钱,库函数和HAL库的例程到处都有,调试工具链成熟。

更重要的是,这个项目对主控性能要求不高。ADS1292的采样率通常在125到500SPS之间,每个采样点产生双通道共6字节数据,换算下来每秒只有几KB的数据量。STM32F103跑到72MHz,SPI速率调到4.5MHz左右,读取一次数据加上简单处理,CPU占用率很低。心率计算算法也是常规的时域阈值检测,不涉及矩阵运算,Cortex-M3完全跑得动。

当然,如果以后要加WiFi上传、蓝牙传输、TFT屏幕描画波形,F103的资源和外设数量会紧张一些,这时可以换F4系列。但作为项目原型验证,F103绰绰有余。

2. 硬件连接与电路设计要点

2.1 最小系统与ADS1292引脚接线

ADS1292有两种封装,常用的是TSSOP-24,引脚间距0.65mm,手工焊接有点费劲,建议直接买贴好模块的转接板。模块到手后按下面的表格连接STM32F103最小系统:

STM32F103引脚ADS1292引脚说明
PB13 (SPI1_SCK)SCLKSPI时钟
PB14 (SPI1_MISO)DOUT数据输出
PB15 (SPI1_MOSI)DIN数据输入
PB12CS片选,低电平有效
PB0DRDY数据就绪中断,下降沿触发
PB1RESET复位,低有效
3.3VAVDD/DVDD电源,需要分别串联磁珠
GNDAGND/DGND公共地,单点连接

AVDD和DVDD虽然都是3.3V,但建议在供电线上各串联一个磁珠再进芯片,模拟地和数字地在芯片下方单点汇合,避免数字开关噪声串到模拟前端。这块做不好,后面整个波形噪声会非常大,不要指望软件能完全救回来。

2.2 电极线与右腿驱动电路

ADS1292的输入通道有三根关键电极:IN1P接左臂(LA)、IN1N接右臂(RA)、RLDOUT通过电阻接到右腿(RL)。芯片内部已经有RLD放大器,你只需要把RLD输出经过一个100kΩ到200kΩ的电阻接到RL电极上就行。

这个右腿驱动是心电系统里非常巧妙的设计:它把人体共模电压反相放大后送回人体,主动抵消工频干扰。如果你发现波形上叠加了明显的50Hz毛刺,先检查RLD通路是否接触良好,很多时候问题不在程序,而在这根线。

电极片建议用医疗级一次性心电电极,比那种夹子电极稳定得多。皮肤接触之前用酒精棉擦拭一下,稍微干燥1-2秒再贴,能显著降低电极-皮肤接触阻抗,减少运动伪迹。我试过直接用杜邦线加金属夹子,波形惨不忍睹,换电极片之后立竿见影。

2.3 电源设计的两个注意事项

ADS1292对电源质量敏感,尤其是模拟电源纹波。我在调试时遇到过波形上有周期性毛刺,用示波器量3.3V电源,发现纹波有50mV左右——就是LED呼吸灯和板上其他电路引入的。解决办法是给ADS1292单独供电,或者在模块电源入口加一个10uF钽电容加上0.1uF陶瓷电容并联。

另一个坑是REF引脚。ADS1292的参考电压默认用内部参考,输出到REFP引脚,外部需要在REFP和REFN之间接一个1uF电容。这个电容不能省,我试过不接,数据读出来是满量程乱跳,查了半天才发现是参考电压不稳。

3. ADS1292驱动的核心细节

3.1 SPI初始化与寄存器读写

ADS1292的SPI时序有个特点:它需要先发送命令,紧跟着读取或写入数据。芯片要求CPOL=0、CPHA=1,也就是空闲时时钟为低电平、第二个边沿采样。STM32标准库配置方式如下:

SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure);

SPI速度建议先从低速开始,分频到1MHz左右能跑通功能后再提速。ADS1292手册标称最大SPI时钟是10MHz,但实际上我跑到4.5MHz就发现偶尔会丢数据,最后稳定用2.25MHz。这个跟布线、杜邦线质量都有关系,不要盲目追求高速。

读写寄存器的命令格式如下:

// 写寄存器:0x40 + 寄存器地址,后续紧跟寄存器值 // 例:写CONFIG1寄存器,值为0x01 uint8_t cmd[2]; cmd[0] = 0x40 | 0x01; cmd[1] = 0x01; CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 2, 100); CS_HIGH();

读寄存器用0x20加地址,读操作需要先发送命令字节,再发一个任意字节时钟,同时取出MISO上的数据。

3.2 配置寄存器:采样率、增益与通路选择

ADS1292的寄存器组中,影响最大的是CONFIG1、CONFIG2、CH1SET、CH2SET这几个。我梳理一下关键配置逻辑:

  • CONFIG1:配置采样率和时钟模式。采样率低三位决定,内部时钟下常见配置是250SPS或500SPS。我调试时先用500SPS,波形细节更丰富,但噪声也更大;后面心率检测稳定后切回250SPS,数据量小、处理压力低。
  • CONFIG2:配置测试信号和RLD。一般需要打开RLD,在参考设计中会设置bit4(RLD_EN)和bit3(RLD_MEAS)为特定组合,同时关闭测试信号。
  • CH1SET:配置通道1增益和输入通路。增益设为6倍(心电信号较小,默认1倍不够用),输入选择正常电极输入而不是测试信号和短路信号,同时使能该通道。
  • CH2SET:同样配置通道2,如果只用一个通道,可以关闭或者全禁用,减少无效数据量。

完整初始化顺序不能乱:先发SDATAC(停止连续读取模式)命令,这个时候才能写配置寄存器;写完发START命令启动转换;等待DRDY引脚出现下降沿,表示新数据准备好。顺序反了,配置会不生效。

3.3 数据读取与DRDY处理

每个DRDY下降沿代表一组新的ADC数据准备好。读取时一次性读取8字节:第1字节是状态,后面是通道1高/中/低字节、通道2高/中/低字节。如果只用通道1,读8字节然后只取通道1数据即可。

uint8_t buf[8]; CS_LOW(); uint8_t wbuf[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; HAL_SPI_TransmitReceive(&hspi1, wbuf, buf, 8, 100); CS_HIGH(); int32_t ch1 = ((int32_t)(buf[1] << 16) | (buf[2] << 8) | buf[3]); if (ch1 & 0x800000) { ch1 |= 0xFF000000; // 符号扩展为32位有符号数 }

24位数据最高位是符号位,不要忘了扩展到int32再使用。我这里用的是HAL库示例,换标准库就是同样的收发流程,注意CS的电平控制必须是手动拉低和拉高,不能依赖硬件NSS。

我最初用轮询方式读DRDY,浪费CPU而且容易漏数据。后来改成PB0引脚的下降沿外部中断,在中断服务函数里只做置标志位,主循环检测到标志后统一读数据和处理,效果好了很多。

4. 心电信号处理与心率计算

4.1 为什么不能直接把原始数据送到串口

直接从ADS1292读出来的原始数据虽然已经数字化,但它包含两个问题:一是信号叠加了约几百mV的直流偏置(电极极化电压),二是存在基线漂移和呼吸伪迹,频率远低于心电本身。如果直接把原始数据发到串口,波形看起来像一条上下浮动的粗线,R波峰值不明显,没法做后续检测。

处理的第一步是高通滤波。最简单有效的是移动平均滤波再做减法,相当于去掉信号中的低频成分。比如窗口长度取100个点,用当前值减去窗口平均值,就能有效压低0.5Hz以下的基线漂移。这个频段不影响心电的QRS波群。

第二步是低通平滑。心电信号的有效频率集中在0.5到40Hz,但肌电干扰和电源谐波会混入更高频成分。这时候做一个简单的滑动平均,窗口取5-7个点,把高频毛刺磨平,R波边缘会变得干净很多。

注意这些滤波会引入群延迟,窗口越长延迟越明显。所以滤波窗口要尽量短,保证实时性。250SPS采样率下,20点的移动平均延迟约40ms,完全能接受。

4.2 R波检测与时域阈值法

心率计算的本质是识别R波峰值出现的时间间隔。经典算法是Pan-Tompkins,包括带通滤波、差分、平方、移动窗口积分,效果最好但计算量大,在F103上也能跑,不过为了简单可靠,我用的是简化版时域阈值法。

流程分四步:

  1. 对滤波后的信号做一阶差分,得到每个点的变化率,R波的上升沿会产生很大的正向差分值。
  2. 计算差分值的绝对值,同时用一个滑动峰值记录器持续追踪最近一段时间内的最大值。
  3. 设定自适应阈值,阈值为当前峰值的0.6倍。当差分值超过阈值且当前值大于前一个点的值时,判定为一个候选R波。
  4. 加入不应期限制。检测到一个R波后的300ms内不允许再检测新的R波——这是心电生理上一个硬约束,180毫秒以内不可能出现两次正常心室除极,能有效屏蔽T波误检。

定时器方面,我直接在相邻R波时间点之间用SysTick计时。F103的SysTick配置成1ms中断一次,维护一个全局毫秒计数器。每检测到一个R波,记录当前毫秒数,用两个间隔和上次的间隔取平均值得到RR间期,心率就是60000除以RR间期。

实测下来,静息状态下稳定时心率误差在±2次/分左右,呼吸引起的窦性心律不齐也能反映出来。

4.3 心率更新策略

心率数值如果每个RR间期都刷新一次,屏幕上数值会跳得非常厉害。体验很差。我改成滑动窗口策略:连续保存最近5个RR间期,去掉最大值和最小值,剩下3个取平均,再换算成每分钟心率。这样既保留了对心率变化的响应速度,又不会因为单次误检导致数值大幅度波动。

实际体验下来,心率值大约每2到3秒刷新一次,在串口助手里显示很稳定。如果想要更平滑的曲线效果,可以改成指数加权平均,但核心思路一样。

5. 串口协议与上位机波形展示

5.1 自定义帧格式设计

串口传输不能裸发原始数据,必须包装成帧。我设计的协议很简单但够用:

字段长度说明
帧头2字节0xAA 0x55
数据类型1字节0x01表示心电波形,0x02表示心率
数据长度1字节后面数据的字节数
数据N字节波形数据或心率值
校验1字节CRC8或者简单的异或和

波形数据我压缩成单字节有符号数,250SPS采样率下,每秒只需要发送250字节,再加上帧头,115200波特率完全够用。心率数据每半秒发一次,数据部分是两个字节的无符号整数。

这里有个小技巧:波形数据在发送前先除以一个缩放系数,把动态范围压到-128到127之间。心电原始数据范围可能在几万左右,直接发需要3个字节,压缩后一字节搞定,串口压力骤减。

5.2 STM32端发送逻辑

发送采用环形缓冲区加DMA的方式。主循环里计算完心率、处理好波形数据后,把待发送的数据帧写入环形缓冲区,串口DMA传输空闲时自动从缓冲区取数据发送。这样处理期间不会被串口阻塞,保证心电数据不丢帧。

void SendWaveformFrame(int8_t data) { uint8_t frame[6]; frame[0] = 0xAA; frame[1] = 0x55; frame[2] = 0x01; frame[3] = 0x01; frame[4] = data; frame[5] = frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4]; UART_SendBytes(frame, 6); }

波特率设置115200,8位数据、1位停止位、无校验。这是串口调试助手的默认配置,通用性最好。

5.3 串口助手的波形显示心得

显示波形我试过两种方式。如果只是想快速看效果,用支持波形显示的串口助手如VOFA+或SerialPlot,按照协议把模拟通道映射到数据字节上,几秒钟就能把波形画出来。这类软件有单独的协议模式,配置好帧格式和通道映射后,平滑度不错。

如果想把波形和心率显示集成到一个界面上,用Qt写一个自定义串口上位机最灵活。我后来做的是Python的pyserial加matplotlib动态绘图,刷新率不高但胜在开发快,适合原型验证。不管哪种方式,核心都在于串口协议的一致性,STM32端发的帧格式和上位机解析逻辑必须严格对齐,否则全是乱码。

6. 调试踩坑记录与经验总结

6.1 波形基线漂移

项目初期最大的问题是波形基线上下漂移,幅度很大,甚至漂出屏幕。排查下来有三个原因:一是电极接触不良,二是高通滤波截止频率太低,三是体动产生的低频干扰。

我把移动平均窗口从150点缩短到80点,高通截止频率从约0.7Hz提升到1.5Hz左右,基线漂移问题明显改善。代价是心电ST段略有变形,但对心率检测来说完全不影响。

6.2 50Hz工频干扰怎么消除

即使有右腿驱动,工频干扰也不可能完全消除。如果干扰在可接受范围内,用软件50Hz陷波滤波即可解决。但陷波滤波如果做得太尖锐,会产生振铃效应,在R波附近出现伪波形。我的做法是先检查右腿驱动的电极是否贴牢,再用屏蔽线替换普通杜邦线,最后才考虑软件陷波。

屏蔽线的屏蔽层一端接地,另一端悬空,我是把STM32端的地和ADS1292模块的地连通后,屏蔽层单端接到系统GND,干扰进一步减小。

6.3 常见问题速查表

现象原因解决方式
读出的数据全为0或满量程SPI极性配置错误、CS时序不对检查CPOL=0、CPHA=1,确认CS手动拉低期间完成整帧收发
波形有大量毛刺AVDD供电纹波大、SPI线过长磁珠隔离模拟电源、缩短导线、降低SPI速率
波形严重漂移无法固定电极接触不良、高通滤波窗口过短更换电极片、酒精清洁皮肤、增大移动平均窗口
心率偶尔翻倍或减半R波阈值为固定值、T波被误检改成自适应阈值、缩短窗口、增加不应期限制
串口波形断断续续环形缓冲区溢出、DMA配置错误增大缓冲区、检查串口DMA空闲中断是否正常触发
上电后DRDY一直不跳变RESET时序没满足、START命令未发送确保上电后RESET拉低至少2us再拉高,按顺序发送SDATAC、配置、START

6.4 几个能提升体验的改进点

波形显示里面,适当做一下衰减和偏置调整会直观很多。我在上位机里加了纵向缩放和自动基线归中,不管原始信号幅度多大,都能完整显示在窗口里。

电源方面,如果以后做实物接入人体,强烈建议用电池供电而不是USB供电,然后把USB地线和模拟地做隔离。我试过USB供电时的波形质量和电池供电时差距明显,主要是USB电源的开关噪声耦合到了人体。

最后一个建议:一定准备一个心电图模拟器,或者至少是稳定的测试信号源,ADS1292寄存器里就自带测试信号,可以用它来验证整套链路是否正常。这样可以排除“是不是我程序读数据不对”和“是不是电极没贴好”两个变量,调试效率能提升一倍。

做完这个项目我的体会是,心电采集并不是一个“代码写完就完事”的活,硬件和算法的磨合占了大半时间。只要把ADS1292的寄存器吃透、SPI时序理清楚、滤波和R波检测做到位,整个系统跑起来只是时间问题。后续如果继续做,可以加上蓝牙传输到手机端、TFT屏幕直接描波形、SD卡存储心电数据等,都是一个框架内可以扩展的方向。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询