ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103+ADS1292心电采集与心率实时监测系统设计

STM32F103+ADS1292心电采集与心率实时监测系统设计 简介这套工程面向使用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跑到72MHzSPI速率调到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时序有个特点它需要先发送命令紧跟着读取或写入数据。芯片要求CPOL0、CPHA1也就是空闲时时钟为低电平、第二个边沿采样。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在参考设计中会设置bit4RLD_EN和bit3RLD_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上也能跑不过为了简单可靠我用的是简化版时域阈值法。流程分四步对滤波后的信号做一阶差分得到每个点的变化率R波的上升沿会产生很大的正向差分值。计算差分值的绝对值同时用一个滑动峰值记录器持续追踪最近一段时间内的最大值。设定自适应阈值阈值为当前峰值的0.6倍。当差分值超过阈值且当前值大于前一个点的值时判定为一个候选R波。加入不应期限制。检测到一个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); }波特率设置1152008位数据、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时序不对检查CPOL0、CPHA1确认CS手动拉低期间完成整帧收发波形有大量毛刺AVDD供电纹波大、SPI线过长磁珠隔离模拟电源、缩短导线、降低SPI速率波形严重漂移无法固定电极接触不良、高通滤波窗口过短更换电极片、酒精清洁皮肤、增大移动平均窗口心率偶尔翻倍或减半R波阈值为固定值、T波被误检改成自适应阈值、缩短窗口、增加不应期限制串口波形断断续续环形缓冲区溢出、DMA配置错误增大缓冲区、检查串口DMA空闲中断是否正常触发上电后DRDY一直不跳变RESET时序没满足、START命令未发送确保上电后RESET拉低至少2us再拉高按顺序发送SDATAC、配置、START6.4 几个能提升体验的改进点波形显示里面适当做一下衰减和偏置调整会直观很多。我在上位机里加了纵向缩放和自动基线归中不管原始信号幅度多大都能完整显示在窗口里。电源方面如果以后做实物接入人体强烈建议用电池供电而不是USB供电然后把USB地线和模拟地做隔离。我试过USB供电时的波形质量和电池供电时差距明显主要是USB电源的开关噪声耦合到了人体。最后一个建议一定准备一个心电图模拟器或者至少是稳定的测试信号源ADS1292寄存器里就自带测试信号可以用它来验证整套链路是否正常。这样可以排除“是不是我程序读数据不对”和“是不是电极没贴好”两个变量调试效率能提升一倍。做完这个项目我的体会是心电采集并不是一个“代码写完就完事”的活硬件和算法的磨合占了大半时间。只要把ADS1292的寄存器吃透、SPI时序理清楚、滤波和R波检测做到位整个系统跑起来只是时间问题。后续如果继续做可以加上蓝牙传输到手机端、TFT屏幕直接描波形、SD卡存储心电数据等都是一个框架内可以扩展的方向。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进