资讯动态

STM32与ADS1292心电采集实战:从SPI时序到R波检测

发布时间:2026/10/16 13:59:09 来源:尧图企业网站定制
简介针对STM32F103微控制器与ADS1292生物信号调理芯片实现的心率实时显示及心电图波形捕获方案面向嵌入式开发、生物医学信号处理学习者及医疗健康类项目开发者。工程以标准库组织完整覆盖ADS1292寄存器配置、心电信号采集、片内可编程增益与滤波调节、STM32F103的R波检测、R-R间期计算以及UART串口上报链路配合串口助手即可实时显示心率数值与心电图波形。调试中可借助ADS1292最高500Hz采样率捕捉细节并依据源码注释理解信号预处理、R波识别算法及心率换算逻辑。资源共207个文件压缩包约6.39MB以C语言源文件、头文件及Keil工程配置为主另有编译生成的hex/axf/map烧录调试文件、批处理脚本和说明文本工程结构清晰。已有7925人学习下载。获取后可直接打开工程烧录验证也可修改滤波策略与通道配置用于双导联采集适合课程设计、健康监测或嵌入式生物信号处理入门。1. 别急着调滤波器先把让 ADS1292 开口说话的路走通第一次把 STM32F103 和 ADS1292 焊在一块时我心想最难的一定是信号处理。结果整整两天串口收不到任何像样的心电数据SPI 读回来的全是 0xFF。后来才发现ADS1292 这种医疗级前端芯片真正拦住新手的不是滤波算法而是它那一套严格的上电时序、SPI 主从约定和数据就绪信号。这篇笔记把从硬件连线、寄存器配置到 R 波检测、心率计算、串口波形的完整链路写下来照着做能少走一半弯路。无论你是做课程设计、健康手环原型还是心电图采集实验这条路都适用。下面从最容易被忽略的电源和 SPI 时序开始。2. 硬件连接与电平匹配决定项目成败的第一道门槛ADS1292 是 TI 出品的双通道 24 位低功耗生物电模拟前端内部集成了 PGA、右腿驱动和导联脱落检测。STM32F103 负责通过 SPI 配置寄存器、读取转换结果再把处理后的数据和心率通过串口发给上位机。很多教程只给一张引脚表就让你焊板子但电平、供电顺序和 SPI 极性这三件事才是真正的分水岭。2.1 最小系统连接6 根线其实暗藏 4 个细节我通常用一块 3.3V 的 STM32F103 最小系统板配合 ADS1292 模块。连接关系如下表STM32F103 引脚ADS1292 引脚说明PA5SPI1_SCKSCLK约 2MHz 起步后续可提速PA6SPI1_MISODOUTADS1292 输出数据PA7SPI1_MOSIDIN配置命令写入PB0普通 GPIO 输出CS低电平片选有效PB1外部中断输入DRDY数据就绪下降沿触发PB12普通 GPIO 输出RESET低电平复位这里四个细节值得专门说。第一CS 不要接到 SPI 硬件 NSS因为 ADS1292 要求 CS 在每次通信之间拉高硬件 NSS 在这种低速设备上容易产生毛刺。第二DRDY 必须接 EXTI 引脚因为连续转换模式下DRDY 每次下降沿代表一组新数据准备好了轮询会占用大量 CPU 且容易漏数据。第三SCLK 初始配置成 2MHzADS1292 手册标注最高支持 20MHz但 STM32F103 的 SPI 在全双工模式下超过 9MHz 时时序余量变小串扰和线路长度会开始捣乱。第四RESET 脚必须由软件控制因为芯片有上电复位时序要求单纯靠 RC 硬复位在电压爬升慢的场景会失败。2.2 电源和共地示波器上看到的“鬼波形”源头ADS1292 的模拟电源 AVDD 需要 2.7V 到 3.6V我直接接 3.3V数字电源 DVDD 也接 3.3V这样和 STM32F103 的逻辑电平完全兼容不需要电平转换。但要注意ADS1292 的 AVDD 和 DVDD 虽然是两个引脚在模块上通常已经做了 LDO 隔离如果你是从 STM32 板上引出 3.3V务必保证两块板的地线先在物理上靠近的位置连好再用一条独立导线接电源。否则 ADC 参考地不干净采出来的波形会叠加上周期性的低频漂移看起来像基线在慢慢呼吸这时候很多人会误判成算法问题实际是共地电阻太大。另一个容易踩的是参考电压。ADS1292 内部参考默认开启电压 4.2V不对ADS1292 内部参考是 2.4V这点很关键。查手册ADS1292 的内部参考电压是 2.4V 或者 4.0V具体而言CONFIG3 里的 REFP 位用于选择参考电压是否输出到引脚。稳妥做法是让我先把 CONFIG3 设为 0x20即开启内部参考并缓冲到引脚同时把 VREF 引脚对地加一个 0.1uF 去耦电容。我见过有人因为模块上没有 VREF 电容导致波形幅值忽大忽小。所以供电建议是AVDD、DVDD 分别加 10uF 钽电容并联 0.1uF 陶瓷电容靠近芯片放置。3. 让 ADS1292 输出数据寄存器配置与 SPI 读取的最小代码ADS1292 的 SPI 是一种命令-响应机制主机先发送命令字节然后读出数据或写入数据。它的命令表不长但寄存器配置决定工作模式这一步出错时串口只会安静地吐 0x7F 或者 0x00。3.1 初始化序列从复位到自测的完整代码下面是一段基于 STM32 标准外设库风格的初始化代码已经去掉平台无关的杂质核心流程完整。void ADS1292_Init(void) { // 1. RESET 引脚先拉高确保芯片不在复位状态 GPIO_SetBit(GPIOB, GPIO_PIN_12); Delay_MS(50); // 2. 发送软件复位命令 0x06等待内部复位完成 ADS1292_SendCmd(0x06); Delay_MS(20); // 3. 读取设备 ID校验通信是否正常 uint8_t devId ADS1292_ReadReg(0x00); if (devId ! 0x73) // ADS1292 的 ID 固定为 0x73 { // 可以在这里点灯报警说明 SPI 或时序有问题 // 通常我在这里打断点排查 CS 和时钟极性 } // 4. 配置 CONFIG1默认节电模式关闭连续转换模式采样率 250SPS // 寄存器地址 0x01值 0x00 表示 HR1DAQ 模式采样率 fMOD/512 ADS1292_WriteReg(0x01, 0x00); // 5. 配置 CONFIG2关闭测试信号使能内部参考缓冲 // 0x20 0010 0000WCT 接右腿驱动测试信号关闭 ADS1292_WriteReg(0x02, 0x20); // 6. 配置 CONFIG3内部参考使能芯片正常采集模式 ADS1292_WriteReg(0x03, 0xFC); // 0xFC 1111 1100PD_REFBUF1REFINT_EN1PGA 增益为 6 // 7. 配置 CH1SET通道 1 接输入PGA 增益 6正常模式 // 地址 0x07值 0x05 GAIN[2:0]011增益 6 ADS1292_WriteReg(0x07, 0x05); // 8. 配置 CH2SET关闭通道 2 以减少电流避免干扰 ADS1292_WriteReg(0x08, 0x80); // 9. 配置 GPIO 控制寄存器关闭 GPIO 输出 ADS1292_WriteReg(0x0A, 0x00); // 10. 拉高 DRDY 对应的 EXTI 引脚准备接收下降沿 EXTI_EnableLine(EXTI_LINE1); }这段代码的逻辑是先做硬件复位再发软件复位命令让芯片处于已知状态。读取 ID 是验证通信的黄金步骤如果读不到预期的 0x73后面所有操作都是空谈。CONFIG1 中的采样率位 HR 默认是 1意味着用 512 分频高速模式我设为 0 使用 250SPS这是心电检测比较合适的折中既能保住 R 波细节又不会产生太多数据让 STM32 串口处理不过来。CONFIG3 里的 0xFC 同时开了内部参考和右腿驱动这是让波形稳定的关键很多模块散件默认状态并不开启右腿驱动导致人体共模干扰直接灌入。3.2 读取 24 位心电数据核心循环代码与参数边界ADS1292 在连续转换模式下每完成一次采样会拉低 DRDY。主机看到下降沿后通过 SPI 读取 3 个字节得到 24 位有符号数。但这里有个很容易搞错的点ADS1292 的 24 位数据里符号位是最高位负数的值是补码而且在读取数据之前必须先通过命令 0x20 读取状态寄存器确保没有 overrange 错误实际上流程是每次 DRDY 后主机先读 3 字节数据中间不需要额外命令因为 CS 拉低后 DOUT 上会依次移出 24 位数据。为了确保帧同步我建议连续读 2 次数据第一次丢弃第二次使用这样能规避偶发的字节错位。int32_t ADS1292_ReadChannelData(void) { uint8_t buf[3]; int32_t raw 0; // CS 拉低SPI 读取 3 个字节 GPIO_ClearBit(GPIOB, GPIO_PIN_0); for (int i 0; i 3; i) { buf[i] SPI_ReadByte(); } GPIO_SetBit(GPIOB, GPIO_PIN_0); // 拼装 24 位补码转为 32 位有符号数 raw ((int32_t)buf[0] 16) | ((int32_t)buf[1] 8) | buf[2]; if (raw 0x800000) // 符号位扩展 { raw | 0xFF000000; } return raw; }这段代码里CS 拉低后连续读 3 个字节必须在读完最后一位之后再拉高。符号位扩展那一行很多人会漏漏掉之后负半周波形会变成一块奇怪的台阶。为什么要做符号位扩展因为 STM32 的 int32_t 是 32 位而 ADS1292 数据是 24 位补码如果不扩展正数正常负数会变成一个巨大的正数画出来的波形下半部分全部被压缩到零轴附近。读取频率由 DRDY 中断驱动不主动去查询这样 CPU 可以腾出时间做滤波和串口发送。采样率、增益和数据率的关系也需要明确ADS1292 内部调制器的采样率是固定的但输出数据率由配置决定。250SPS 时每个通道每秒产生 250 组数据每组 3 字节。如果开双通道一秒就是 1500 字节加上协议开销115200 波特率完全够用。如果用到 1kSPS 双通道一秒 6000 字节115200 波特率依然可以但留给上位机绘图的缓冲压力会变大。4. 从裸数据到心电波形滤波、R 波检测与心率计算读到的 24 位原始值如果直接丢给串口助手画波形你会看到一条在巨大直流偏置上摆动、带有高频毛刺和 50Hz 工频干扰的曲线。要让波形像教科书上的心电图必须经过直流移除、低通滤波、R 波检测和心率计算四步。4.1 预处理高通加低通让基线平得像桌面心电信号能量集中在 0.05Hz 到 100Hz而人体皮肤接触会产生 200mV 以上的直流偏置。ADS1292 虽然内置了 PGA但直流偏置依然可能占据 ADC 满量程的一半以上。所以第一步要做一个数字高通滤波简单做法是移动平均差值法即原始值减去一段时间窗口内的平均值。#define SAMPLE_RATE 250 #define HIGH_PASS_LEN 25 // 高通窗口长度对应 25/2500.1s int32_t hp_buffer[HIGH_PASS_LEN]; int16_t hp_index 0; int32_t ADSRaw_HighPass(int32_t input) { int32_t sum 0; hp_buffer[hp_index] input; hp_index (hp_index 1) % HIGH_PASS_LEN; for (int i 0; i HIGH_PASS_LEN; i) { sum hp_buffer[i]; } int32_t baseline sum / HIGH_PASS_LEN; return input - baseline; }这里的高通窗口长度是关键参数。25 个采样点对应 0.1 秒窗口高通截止频率约 1.6Hz能滤掉呼吸引起的基线漂移同时保留心电 ST 段。如果你觉得基线还是上下跑把窗口加长到 50但超过 50 后快速动态漂移反而滤不掉因为窗口越长对低频变化的跟随越慢。我一般把这段写在中断外面在收到新数据后同步调用。低通滤波我用的是简单的滑动平均窗口取 5 个点。250SPS 下 5 点平均对应的截止频率约为 35Hz能压掉大部分肌电干扰但会稍微削尖 R 波。如果做心率检测削一点没关系如果是要保存波形做诊断建议用 2 阶巴特沃斯滤波。对于串口助手显示滑动平均已经足够。4.2 R 波检测与心率计算阈值不应期与滑动窗口R 波是心电信号中幅值最大、斜率最陡的波。在单片机里做检测最实用的是自适应阈值法加不应期。int32_t threshold 0; int32_t r_peak_value 0; uint16_t rr_interval 0; uint8_t refactory_count 0; uint8_t DetectRWave(int32_t filtered) { // 动态阈值调整取最近 8 次 R 波峰值的 60% // 同时设置一个最低阈值防止噪声引发误检 if (refactory_count 0) { refactory_count--; return 0; } if (filtered threshold filtered 80) { r_peak_value filtered; rr_interval sample_count_since_last_r; sample_count_since_last_r 0; // 更新阈值取新峰值和历史峰值的滑动平均 threshold (threshold r_peak_value * 6) / 8; refactory_count SAMPLE_RATE * 0.25; // 不应期 250ms return 1; } sample_count_since_last_r; return 0; }阈值初始值我设为 500因为经过 6 倍增益和 24 位 ADC 后R 波峰值通常在 1000 到 3000 之间。这里的 80 是最低检测门限防止 T 波或者噪声被误判。不应期 250ms 对应心率上限 240bpm可以过滤掉 R 波后面的 T 波——T 波虽然比 R 波矮但有时能达到 R 波 50% 的幅值。如果不应期设得太短比如 100ms心率在 200bpm 时会漏数设得太长比如 400ms真性早搏会被漏检。心率计算在检测到 R 波时进行bpm 60 * SAMPLE_RATE / rr_interval这里的 rr_interval 是两次 R 波之间的采样点数。需要提醒的是这种算法对运动伪迹依然敏感。如果只是桌面端演示用这个够用如果需要更高鲁棒性可以加一个幅值突变检测当新 R 波幅值超过阈值 3 倍以上时认为是伪差丢弃这次检测并重新收敛阈值。4.3 串口协议设计让串口助手直接画出波形串口助手的心电图波形显示功能通常接受原始字节流或特定帧格式。我习惯自定义一个帧包含帧头、数据长度、通道数据、心率和校验这样上位机既能画波形又能显示心率。uint8_t tx_buffer[12]; void BuildAndSendPacket(int32_t ecg1, uint16_t heartRate) { // 帧头 0xAA 0x55长度 6数据为 24 位有符号数和心率 tx_buffer[0] 0xAA; tx_buffer[1] 0x55; tx_buffer[2] 6; // 数据长度 tx_buffer[3] (ecg1 16) 0xFF; tx_buffer[4] (ecg1 8) 0xFF; tx_buffer[5] ecg1 0xFF; tx_buffer[6] (heartRate 8) 0xFF; tx_buffer[7] heartRate 0xFF; // 校验长度 数据的累加和 uint8_t checksum 0; for (int i 2; i 8; i) { checksum tx_buffer[i]; } tx_buffer[8] checksum; // 通过 USART1 发送 for (int i 0; i 9; i) { while (USART_GetFlag(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, tx_buffer[i]); } }帧头 0xAA 0x55 用来让上位机做同步防止中途插拔串口导致数据错位。长度字段可以直接写死为 6但保留它可以让未来扩展其他通道。心率是 16 位无符号数如果 bpm 超过 255上位机能正确处理。校验和只算数据和长度不包含帧头这样上位机收到数据后也可以在本地算一次对不上就丢弃当前包有效降低显示乱码概率。注意每次 DRDY 中断里只发送一包不要在一个中断里发多包否则会干扰下一轮 SPI 读取造成数据丢帧。这里刻意没有用文本协议比如 ECG:123 HR:75\r\n。文本协议虽然调试方便但串口助手画波形时需要从字符串里解析数字速度慢且容易因为打印延迟造成波形断断续续。二进制帧是实时显示的正确选择。5. 避坑指南ADS1292 心电图项目的 5 个高频翻车现场这一章把我遇到的、以及身边开发者反馈最多的问题整理成清单每条都按“现象、原因、解决”写出方便你直接对照排查。5.1 三个波形类翻车现场与修复现象一串口助手画出波形但整条曲线像在“呼吸”一分钟内上下漂移两三次。原因基本是基线漂移具体分两种一是电极与皮肤接触阻抗变化二是数字高通滤波器窗口太短。我曾用窗口长度 10对应 0.04 秒结果呼吸频率的干扰滤不掉基线按呼吸节奏上下起伏。解决方法是把高通窗口加到 25 到 40 之间同时涂抹导电膏降低接触阻抗。现象二波形上下削顶看起来像被钳位了而且心率数值偶尔跳变到 199。原因是增益设置过高加上 R 波检测阈值太低。PGA 增益 6 倍时如果电极接触好R 波峰值可能达到满量程的 70%T 波也能达到 30%阈值设 80 时 T 波被误判成 R 波。解决方法是把 CH1SET 的增益降为 4 倍对应 0x04最低检测门限抬到 200再配合 250ms 不应期误判会明显减少。现象三波形在负半轴出现“台阶”看起来像是数据突然变正数然后骤降。原因几乎可以确定是 24 位补码没有做符号位扩展。我刚开始踩过代码里只把三字节拼成 int32没有做if (raw 0x800000) raw | 0xFF000000结果负值变成大约 1600 万的正数串口助手画的图就像一条断崖。解决方法是检查拼接逻辑最好在调试器里单步看 raw 的值呼吸时 raw 应该在正负几百之间而不是恒定巨大正数。5.2 两个通信与电源类故障及定位方法现象四上电后读取设备 ID返回 0xFF 或者 0x00。原因要么是 SPI 时序不对要么是 DRDY/CS 引脚没拉对。我遇到过最隐蔽的是复位后没有等待足够时间ADS1292 内部振荡器起振需要几毫秒立刻读寄存器会失败。解决方法是软件复位后延时 20ms再读 ID如果还是 0xFF用示波器探测 CS 在通信时是否被拉低DOUT 上是否有时钟边沿对应的数据。也可以用一根杜邦线把 DIN 和 DOUT 短接发一个字节看是否原样返回排除 SPI 初始化的低级问题。现象五心率显示稳定但波形上叠加了周期性的尖刺每秒钟出现几次。原因可能是 STM32 在 SPI 读取期间被串口发送中断打断导致 CS 时序被拉长。SPI 读取 3 字节虽然很快但如果串口中断优先级高于 SPI且串口发送是阻塞方式中断嵌套会让 CS 拉低时间超过几个毫秒ADS1292 内部状态机可能错乱下一个数据帧自动丢一个字节。解决方法是调整中断优先级让 EXTIDRDY优先级最高SPI 读取期间临时关闭更高优先级串口中断或者在读取数据时用 SPI 硬件 NSS 管理多余的时序。更稳妥的方案是把串口发送改成 DMA发送不占 CPU就不存在中断打断的问题。5.3 一条快速排查路径从现象到根因的三步法如果以上现象都不完全匹配我习惯按这个顺序排查先用示波器看 DRDY 是否有周期下降沿正常时它应该按采样率稳定拉低比如 250SPS 时每 4ms 出现一次。如果 DRDY 频率不对说明 CONFIG1 配置错误或时钟晶振有问题。第二步看 SPI 读回的原始数据把符号位扩展后的 raw 通过串口原样输出人为用手碰触导联看数值是否在正负 5000 以内变化如果毫无变化多半是模拟前端的输入引脚没有正确连接电极。第三步检查电源纹波用手摸芯片外壳波形若有很大变化说明共地不牢或者电源去耦不足。三步走完90% 的问题都能定位。6. 进阶技巧让波形更丝滑、心率更可信的检查清单当你已经能稳定显示心电图波形下一步就是验证数据的正确性和优化体验而不是停下来庆祝。我做这个项目到最后发现真正拉开差距的是两个细节一是如何确认自己看到的波形是真实心电而不是噪声二是如何在不增加硬件成本的情况下提升实时性。先说说验证方法。最靠谱的是用手头信号发生器输出一个 1Hz 正弦波幅度 2mV通过一个 1:1 隔离电阻接到 ADS1292 输入此时串口画出的波形应该是一个干净的正弦波频率对应 60bpm。如果没有信号发生器就用手指分别按住两个电极观察波形幅度是否随按压力度变化。但要注意用手指测试时 R 波不一定出现因为手部肌肉电位干扰很大看到的多半是肌电毛刺。我一般会用两节 5 号电池串联一个 50kOhm 电位器输出约 1.2mV 到 3mV 的直流叠加一个交流纹波先验证 ADC 通道增益是否正确。关于实时性有一个容易忽视的瓶颈STM32F103 的 SPI 读取使用的是阻塞循环读取 3 字节本质上要等待 24 个时钟周期在 2MHz 下大约 12us这个损耗可以忽略。但如果为了显示高质量波形而把采样率提高到 1kSPSCPU 每毫秒要处理一次中断而且每次中断里要完成高通滤波、低通滤波和串口发送如果使用 115200 波特率发送 9 字节大约需要 0.8ms这几乎占满了整个采样周期。我的习惯是开一个环形缓冲区DRDY 中断里只做 SPI 读取和滤波预处理串口发送交给主循环或 DMA。用 DMA 发送串口数据是性价比非常高的优化改配置后 CPU 占用率从 80% 降到 30%。最后想讲一个我自己的教训曾经为了追求波形漂亮把低通滤波窗口加到 20 个点结果 R 波被磨圆了心率检测从 T 波之后的后一个 P 波开始偶尔误检。后来我意识到串口助手显示的波形和用于心率计算的波形不该是同一路。显示路径用滑动平均 5 点心率路径用原始数据经过 50Hz 陷波后直接检测 R 波这样两边都达到最佳效果。在不改变硬件的条件下把这个分流处理做好整个系统的稳定度会有一个肉眼可见的提升。这套基于 STM32F103 和 ADS1292 的方案从读通 SPI 到画出可信心电图每一步都有明确验证标志照着流程走完你会发现“玄学”其实都是参数没到位。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑