简介:一款基于STM32F103与ADS1292的心率实时显示与心电波形捕获工程,面向嵌入式开发者和生物信号处理学习者,提供从信号采集、处理到上位机显示的完整方案。项目利用ADS1292高精度低噪声特性、可编程增益和滤波功能采集心电信号,STM32F103作为主控完成数字滤波、识别R波和计算R-R间期,心率结果及波形数据经串口发送至串口助手实时呈现,既可学习又可作二次开发基础。压缩包共207个文件、6.39MB,以C源码、Keil工程配置、编译输出文件为主,还包含调试记录、链接脚本与辅助批处理,结构较完整,方便直接打开工程对照阅读。资源已有约7900人次学习,源码注释对ADS1292寄存器配置、滤波器设计、心率统计算法以及串口收发逻辑均有清晰说明,适合理解心电检测系统的工程实现,并可根据需要移植到其他生理监测场景。 去年帮一个学弟调心电项目,他在ADS1292的寄存器配置上折腾了整整三天,串口助手刷出来的数据全是乱码。排查到最后发现是SPI时序的问题——芯片在等数据准备好,主控这边已经在发下一个命令了。这种问题很典型,因为ADS1292这类生物电模拟前端芯片确实好用,但它的数据手册信息量极大,新手很容易在驱动和协议设计上栽跟头。
这个项目的核心链路其实很清晰:用STM32F103读取ADS1292采集到的心电信号,通过串口发到PC端串口助手,实时显示心电图波形和心率数值。对想快速跑通生物电采集但没有太多模拟电路经验的人来说,这是一个性价比极高的入门组合——ADS1292把最难的模拟前端问题解决了,STM32F103又是普及率最高的教学级主控,资料多、调试工具全。这篇文章我会把硬件接线、寄存器配置、数据解析、R波检测和串口协议设计整条链路完整走一遍,给出可以直接抄作业的关键代码和帧格式,也会把实测中常踩的坑和排查思路写清楚,希望能帮你少走几步弯路。
1. 为什么这个组合能成为心电采集的入门首选
1.1 ADS1292到底帮你省了什么
做心电采集最麻烦的不是后端数字处理,而是前端模拟链路。人体心电信号幅度只有0.5mV到4mV左右,还叠加了50Hz工频干扰、肌电噪声、运动伪差和各种直流偏置,要想在ADC输入端拿到干净的信号,传统方案需要自己搭仪表放大器、高通滤波器、低通滤波器、右腿驱动电路,每一级都要考虑噪声、共模抑制比和阻抗匹配,调起来非常痛苦。
ADS1292把这些东西全部集成进了一颗芯片。它里面有两个24位高精度ADC通道,内置可编程增益放大器(PGA),增益可以从1倍到12倍调节,还集成了右腿驱动放大器、导联脱落检测、内部偏置发生器、内部测试信号源。默认配置下,只需要接上电极、加上去耦电容,就能从SPI接口读出可以直接用于显示的心电数据。可以说它在芯片层面帮我们解决了信号链路上百分之八十的难题。
另一个很关键的点是它的功耗。ADS1292正常工作模式整颗芯片电流大概在几百微安级别,待机模式下更低,用STM32F103这种低功耗主控配合,整体可以用电池供电,这为将来做便携式设备打下了基础。
1.2 STM32F103在这个项目里的角色定位
STM32F103在这个项目里不承担任何模拟信号处理工作,它的核心任务有三个:第一,通过SPI接口配置ADS1292的寄存器并读取转换结果;第二,对读到的24位原始数据做必要的数据处理,比如波形显示前的缩放和心率计算;第三,通过串口把波形数据和心率值按约定的帧格式发送给PC端。
这颗芯片的SPI时钟最高可以到18MHz,而ADS1292在SPI模式下的最大时钟速度是10MHz左右,主控性能完全够用。实际项目中我建议用1MHz到4MHz的SPI时钟,太低会导致读数据时间过长,太高容易因为排线干扰出现通信错误。
用STM32F103还有一个优势是它的生态。标准外设库资料极多,网上随便一搜就是各种初始化模板,GPIO、SPI、串口的配置方式大家都很熟悉,遇到问题容易找到参照。如果你用的是STM32CubeMX生成代码,初始化部分甚至可以自动完成,自己只需要写ADS1292的驱动逻辑。
1.3 适合谁照着做,不适合谁
这个项目最适合两类人:一类是想做心电、肌电、脑电类课程设计或毕设的在校学生,需要快速跑通一条完整的数据链路;另一类是做可穿戴设备预研的工程师,想评估ADS1292这颗芯片的实际表现,但暂时不想自己画板子,先用模块和数据线把原理验证做了。
但不适合一类人——想在几天内做出一个医疗级心电图机的朋友。ADS1292本身性能很好,但模块上用的电极线、屏蔽、以及后续的滤波算法和心率变异性分析,都不是这个入门链路能覆盖的。这个项目的定位是把数据采出来、波形显示出来、心率算出来,作为工程验证和算法研究的基础平台,而不是临床设备。
2. 硬件连接的几个关键点,照着接不出信号问题
2.1 最小系统板与ADS1292模块的接线全景
一般大家在淘宝上买的ADS1292模块是拆机芯片做的,引脚已经引出来,配合一个STM32F103C8T6最小系统板就能跑。接线方案如下表所示:
| STM32F103 | ADS1292模块 | 说明 |
|---|---|---|
| PA5 (SPI1_SCK) | SCLK | SPI时钟 |
| PA6 (SPI1_MISO) | DOUT | 数据输出,主控接收 |
| PA7 (SPI1_MOSI) | DIN | 数据输入,主控发送命令 |
| PA4 (任意GPIO) | CS | 片选,低电平有效 |
| PB0 (任意GPIO) | DRDY | 数据准备好标志,下降沿有效 |
| 3.3V | VDD | 模块供电 |
| GND | GND | 共地 |
| 3.3V(或3.3V转1.8V) | AVDD | 模拟供电(视模块具体设计而定) |
特别注意AVDD和DVDD的区别。ADS1292的数字供电DVDD通常需要1.8V,但市面上多数模块在板上已经做好了电平转换,对外只暴露3.3V或5V接口,所以买模块时一定看清楚说明书是“宽电压兼容”还是“必须单独接1.8V”。
DRDY引脚是整个通信流程的节拍器。ADS1292完成一次AD转换后会把DRDY拉低,表示数据已经在输出寄存器里准备好了,主控可以读走。读数据完成后DRDY自动恢复高电平。这个引脚必须接到STM32的普通GPIO上,用轮询或者外部中断方式处理,绝对不要漏接——漏接的话你永远不知道该什么时候去读数据。
2.2 模拟前端的供电和地去耦细节
大部分ADS1292模块的问题不在接线对不对,而在供电质量。心电信号本身太微弱,电源纹波对它的影响会直接体现在波形上。实测下来,如果直接用STM32最小系统板的3.3V给模块供电,波形上经常能看到明显的锯齿状干扰,尤其是板载LDO质量一般、USB供电纹波大的时候。
更好的做法是模块单独用一块低噪声LDO供电,比如AMS1117-3.3或者更专业一点的LP5907,输入用锂电池或者USB,输出并联一个10uF钽电容和一个0.1uF陶瓷电容,分别负责低频储能和高频去耦。如果模块上已经有完整的供电设计,那就保持原样,但去耦电容尽量不要去掉。
地线方面,心电采集链路是典型的模拟和数字混合系统。模块部分属于模拟地,STM32最小系统板属于数字地,两者用一根导线单点连接通常就够了。如果模块上AVDD和DVDD是分立的引脚,GND实际上也是分开的两个引脚,那一定要仔细看模块原理图,确认模拟地和数字地是否已经在模块上连在一起了。如果没连,你需要用0欧电阻或者磁珠桥接,而不是用飞线乱接。
2.3 导联、右腿驱动和人体接触的安全提示
ADS1292内置右腿驱动电路,用来降低人体共模电压并抑制50Hz工频干扰。接线时,右腿驱动电极RLD需要单独接一个电极片,贴在右腹部或者右腿位置,三条导联线分别是左臂、右臂和右腿驱动。
关于人体接触这块必须多说一句:这个项目允许用电池或者未连接市电的笔记本电脑进行测试,但不建议一边插着充电器一边让人体接触电极。虽然ADS1292内部有一定的ESD保护,但心电采集系统最担心的就是漏电风险。用电池供电的笔记本最安全,其次是插着充电器但整机绝缘良好的台式机,但也最好加一个隔离模块再用。另外,每次测试前检查导联线有没有破损,这是长期做生物电实验的基本素养。
3. 寄存器配置和SPI读取:驱动代码的核心逻辑
3.1 初始化流程:从复位到连续采集
ADS1292的初始化流程可以归纳为几个固定步骤:上电复位、发送STANDBY进入待机、写入寄存器配置、发送RDATAC进入连续采集模式。每一步之间都要留出足够的延时,因为芯片内部的各个模块上电和稳定需要时间。
复位有两种方式:硬件复位引脚拉低至少两个时钟周期再拉高,或者发送0x06命令字进行软件复位。模块上一般没有引出复位引脚,所以用软件复位即可。复位后建议延时至少20ms再操作寄存器。
关键寄存器的配置我整理了一下:
| 寄存器 | 地址 | 推荐值 | 含义 |
|---|---|---|---|
| CONFIG1 | 0x01 | 0x03 | 连续采集模式,内部时钟,采样率500SPS |
| CONFIG2 | 0x02 | 0x20 | 开启内部测试信号(测试阶段用,正常采集设为0x00) |
| LOFF | 0x03 | 0x00 | 关闭导联脱落检测 |
| CH1SET | 0x04 | 0x60 | 通道1增益12倍,正常输入 |
| CH2SET | 0x05 | 0x60 | 通道2增益12倍,正常输入 |
| RLD_SENS | 0x06 | 0x2C | 右腿驱动使能,信号源来自两通道 |
| CONFIG3 | 0x0E | 0xCC | 参考缓冲使能,右腿驱动使能,内部参考 |
采样率设置在CONFIG1的低三位,500SPS对应的是011。这个采样率对心电来说已经足够了,因为QRS波群的频率成分大多在20Hz上下,500SPS能采到非常好的波形细节,而且每个采样点之间的时间间隔是精确的2ms,方便后续做R波间期的计算。
CH1SET和CH2SET配置要注意增益后面的bit3是输入短接控制,bit0是导联检测电流源,正常采集时这些位都设为0。两个通道的PGA增益先设为12倍(0x60),因为心电信号较弱,后期如果发现波形饱和削顶再降低增益。
3.2 24位数据的读取和解析
ADS1292的ADC输出是24位有符号数,以二进制补码形式存储,存储格式是高字节在前。每次读数据前,芯片会把有效的24位数据填充到输出寄存器中,一旦开始读取,这些数据就会被锁定,直到读取完成。
解析的时候要特别注意符号扩展的问题。直接把三个字节拼成一个32位数,最高位是符号位,如果最高位为1,说明这个数是负数,需要把32位整数的最高字节全部填充为0xFF,也就是所谓的符号扩展。否则负数会被读成一个很大的正数,波形会完全错乱。
我用的解析方式是先把三个字节左移拼接到32位容器里,然后再左移8位、右移8位,利用C语言的算术右移特性完成符号扩展。这样无论编译器是几位的,结果都是正确有符号数。
3.3 关键代码:SPI读写和DRDY处理
初始化SPI时,需要注意ADS1292要求的时钟极性和相位。根据数据手册,CPOL = 0(空闲低电平)、CPHA = 1(第二个边沿采样)是标准配置。如果你发现读回来的数据全是0xFF或者0x00,先检查CPHA是不是设置成了第一个边沿采样。
下面是一份我实际跑通过的驱动核心代码:
// SPI1 初始化,主模式,1MHz 分频,CPOL=0 CPHA=1 void ADS1292_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); 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_32; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }// SPI 收发一字节 uint8_t ADS1292_SPI_ReadWriteByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); } // 写寄存器 void ADS1292_WriteReg(uint8_t reg, uint8_t value) { ADS1292_CS_LOW(); ADS1292_SPI_ReadWriteByte(0x40 | (reg << 1)); // WREG 命令 ADS1292_SPI_ReadWriteByte(value); ADS1292_CS_HIGH(); } // 读一帧数据,ch1 和 ch2 为输出 void ADS1292_ReadData(int32_t *ch1, int32_t *ch2) { uint8_t buf[9]; uint8_t i; while (GPIO_ReadInputDataBit(ADS1292_DRDY_PORT, ADS1292_DRDY_PIN) == 1); ADS1292_CS_LOW(); ADS1292_SPI_ReadWriteByte(0x12); // RDATA 命令 for (i = 0; i < 9; i++) { buf[i] = ADS1292_SPI_ReadWriteByte(0x00); } ADS1292_CS_HIGH(); *ch1 = ((int32_t)((buf[0] << 16) | (buf[1] << 8) | buf[2]) << 8) >> 8; *ch2 = ((int32_t)((buf[3] << 16) | (buf[4] << 8) | buf[5]) << 8) >> 8; }DRDY的等待我用了轮询方式,因为ADS1292的数据率在500SPS时大约是2ms一个点,主循循环里轮询不会造成明显的CPU浪费。如果你想用中断方式,可以把DRDY接到STM32的外部中断引脚上,在中断服务函数里读取数据。但要注意中断服务函数里不要做耗时的数值运算和串口发送,只做数据搬运,处理逻辑放到主循环里。
4. 在单片机上算心率:R波检测的轻量级实现
4.1 为什么选R波而不选FFT
有人会问,能不能通过傅里叶变换算心率?能,但不适合在这个场景里做。STM32F103的主频只有72MHz,FFT运算虽然有官方DSP库可以用,但需要先积累一段时间的采样数据,再做一次变换取峰值频率,这带来的延迟和计算开销对实时显示来说不太划算。更重要的是,FFT在心率估计上精度有限,分辨率和窗长强相关,动态变化时响应速度也慢。
更直接的方案是时域上的R波检测。心电信号中QRS波群的R波是幅度最大、斜率最陡的波形特征,在多数导联下都比P波和T波明显得多。只要检测到R波,两个相邻R波之间的时间间隔就是一次心跳的RR间期,心率就是60000除以RR间期(单位毫秒)。
4.2 差分阈值法的代码思路
最简单的R波检测思路是差分阈值法。对信号做一阶差分,也就是当前采样点减上一个采样点,差分值在R波上升沿会有一个明显的正向尖峰,在R波下降沿会有一个明显的负向尖峰。当检测到一个超过正向阈值的尖峰后,继续等下一个低于负向阈值的尖峰,就认为检测到了一个完整的R波。
这个方法的真正难点在阈值怎么定。R波幅度会随着电极接触状态、呼吸、运动而变化,固定的阈值在信号变弱时会漏检,在噪声大时会误检。我采用的策略是动态阈值:维护一个历史R波峰值数组,每次检测到新的R波就更新峰值,阈值取历史峰值的一半。
#define SAMPLE_RATE 500 // 采样率 #define MIN_RR_MS 300 // 最小RR间期,对应200bpm #define MAX_RR_MS 2000 // 最大RR间期,对应30bpm #define R_BUFFER_SIZE 8 // RR间期滑动窗口 static int16_t last_sample = 0; static uint8_t state = 0; static uint32_t last_r_time = 0; static int16_t r_peak_buffer[R_BUFFER_SIZE]; static uint8_t r_peak_idx = 0; static uint16_t rr_buffer[R_BUFFER_SIZE]; static uint8_t rr_idx = 0; uint16_t HR_Calculate(int16_t ecg_sample, uint32_t current_ms) { int16_t diff = ecg_sample - last_sample; uint16_t heart_rate = 0; int16_t threshold = 0; int16_t peak_sum = 0; uint32_t rr_sum = 0; uint16_t rr = 0; uint8_t i; last_sample = ecg_sample; // 用历史峰值的均值做动态阈值 if (r_peak_idx > 0) { for (i = 0; i < r_peak_idx; i++) { peak_sum += r_peak_buffer[i]; } threshold = (peak_sum / r_peak_idx) / 2; } if (state == 0) { if (diff > threshold && threshold > 0) { state = 1; } } else if (state == 1) { if (diff < -threshold) { state = 0; rr = current_ms - last_r_time; if (rr > MIN_RR_MS && rr < MAX_RR_MS) { rr_buffer[rr_idx] = rr; rr_idx = (rr_idx + 1) % R_BUFFER_SIZE; rr_sum = 0; for (i = 0; i < R_BUFFER_SIZE; i++) { rr_sum += rr_buffer[i]; } heart_rate = 60000 / (rr_sum / R_BUFFER_SIZE); } last_r_time = current_ms; } } // 更新峰值记录 if (state == 1 && ecg_sample > 0) { r_peak_buffer[r_peak_idx] = ecg_sample; r_peak_idx = (r_peak_idx + 1) % R_BUFFER_SIZE; } return heart_rate; }这个算法的核心思想很简单:先判断信号是否有一个陡峭的正向跳变,再判断是否紧接着有一个陡峭的负向跳变,两者合在一起就构成一次QRS波群的识别。阈值不是固定的,而是从最近几次R波峰值的平均值推导出来的。
4.3 心率值的平滑处理
单次RR间期换算出来的心率会跳得很厉害,因为呼吸、体动都会让RR间期产生几十毫秒的波动。直接显示原始值,你会看到心率在七十到九十之间反复横跳,根本没法看。
处理办法是滑动平均。维护一个8个RR间期的环形缓冲区,每次计算时先求平均RR间期,再用60000除以平均值。这样计算出的心率值会平滑很多,动态响应速度也够用——正常人静止时心率变化本来就慢,8个周期大约对应8秒左右的窗口,适合实时显示。
需要提醒的是不要用算术平均把所有数据相等对待。如果中间混入了一个因为体动导致的异常短RR间期,滑动平均仍然会把异常值带进来。更稳健的做法是排序后去掉最大最小值再平均,也就是所谓的中值-均值混合滤波,但会增加代码复杂度。对于入门项目,滑动平均加合理的心率上下限截断已经足够。
5. 串口协议怎么设计,串口助手上才能又稳定又好看
5.1 帧格式设计的三个核心考虑
把数据通过串口发到PC端,很多人第一反应就是直接printf打印数据,看个大概。但如果你想在串口助手上稳定地显示波形,这样是不行的——打印出来的文本没法被解析成数值波形,而且文本传输效率很低,每秒钟要传几十个字符才能表达一个数值。
设计串口帧格式,我的核心考虑有三个方面:帧同步、数据边界、校验可靠性。
帧同步解决的是“我从哪里开始读一帧”的问题。常见做法是用一两个固定的帧头字节,比如0xA5 0x5A,接收端检测到这两个字节连在一起才认为帧开始。如果只有一个帧头字节,数据正文里偶尔出现相同字节会导致错位。两个字节虽然占用一点带宽,但可靠性高很多。
数据边界解决的是“这一帧有多长、每个字段代表什么”的问题。最简单的办法是用定长帧,每个字段长度固定,帧头后面依次放类型、长度、数据、校验。定长帧的好处是接收端解析逻辑简单,不会出现粘包拆包的烦恼。
校验解决的是“数据传错了怎么办”的问题。我建议用累加校验,也就是把帧头后面的所有字节求和,只保留低8位放进帧尾。接收端把收到的数据同样求和比对,不一致就丢弃整帧。相比CRC16,累加校验实现简单,对串口低误码率的场景足够。
一个实际可行的帧格式如下:
帧头(0xA5 0x5A) + 类型(1字节) + 长度(1字节) + 数据(N字节) + 校验(1字节)类型字段定义:0x01表示波形数据帧,0x02表示心率数据帧。长度字段表示数据段的字节数,校验为除帧头外所有字节的累加和。
5.2 波形数据和心率数据的发送策略
波形数据是典型的周期性高频率数据。采样率500SPS意味着每秒钟要发送500个波形点。每个点用16位有符号数表示,就是每秒1000字节的数据量,在115200波特率下完全扛得住。
但波形点不能一个一个发。每个点单独发一帧,帧头校验的额外开销会占掉一半带宽,而且PC端接收时中断太多,容易丢数据。更好的做法是攒一包再发:每20个采样点组合成一帧波形数据,数据段40字节,每秒发25帧,每帧间隔40ms,接收端能看到平滑的波形曲线。
心率数据的发送频率不需要那么高。每计算出一个新的平滑心率值,大概每秒更新一次就够。可以把心率值塞进波形帧的一种特殊类型里,也可以单独发一帧。我推荐单独发一帧,这样上位机解析代码会更清晰。
串口波特率建议用115200,8位数据位,1位停止位,无校验。在这个波特率下,500SPS的波形数据加上心率数据,串口占用率大概在百分之十几,很充裕。如果采样率提高到1000SPS,一个点用16位表示,同样攒20个点一包,每秒50包,仍然没问题。
波形数据发送示例代码:
#define WAVE_PACKET_SIZE 20 int16_t wave_buffer[WAVE_PACKET_SIZE]; uint8_t wave_count = 0; void SendWavePacket(void) { uint8_t i; uint8_t frame[64]; uint8_t sum = 0; uint8_t len = WAVE_PACKET_SIZE * 2; uint8_t idx = 0; frame[idx++] = 0xA5; frame[idx++] = 0x5A; frame[idx++] = 0x01; frame[idx++] = len; sum += 0x01 + len; for (i = 0; i < WAVE_PACKET_SIZE; i++) { frame[idx++] = (wave_buffer[i] >> 8) & 0xFF; frame[idx++] = wave_buffer[i] & 0xFF; sum += (wave_buffer[i] >> 8) & 0xFF; sum += wave_buffer[i] & 0xFF; } frame[idx++] = sum; for (i = 0; i < idx; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, frame[i]); } wave_count = 0; }发送顺序建议先发波形帧再发心率帧,因为心率帧的实时性要求低,偶尔延迟几十毫秒用户感知不到。另外,如果串口发送阻塞时间太长,会影响ADS1292的数据读取节奏,所以发送函数尽量只做数据搬运,不要做复杂计算,心率计算放在读取数据之后的主循环其他位置。
5.3 串口助手的实际配置与波形查看方法
不是所有串口助手都支持波形显示,所以工具选择决定了你能不能看到漂亮的波形曲线。实测下来,下面几个工具比较推荐:
- XCOM V2.0:经典的串口调试助手,自带波形显示功能,支持波形蓝色/白色切换,解析我们设计的定长帧只需要在“波形显示”里设置对应的解析方式。
- SSCOM:老牌串口助手,功能稳定,但波形显示比较基础,适合简单调试。
- 猫猫串口网络调试助手:界面简洁,支持曲线显示,适合快速验证。
- VOFA+:支持多种协议插件,包括自定义帧格式,画图功能强大,适合做复杂上位机验证。
使用这些工具的通用配置逻辑是:波特率设置成115200,数据位8、停止位1、无校验,打开十六进制显示确认能收到0xA5 0x5A开头的帧,再根据工具支持的协议格式把帧头、类型、长度、数据、校验对应填进去。
如果你用的串口助手不支持自定义波形协议,还有一个笨办法:先用文本模式输出“心率:75 波形:1234”这样的格式化字符串,用曲线显示工具按文本分隔符解析,虽然效率低一点,但验证算法逻辑完全够用。
6. 实测中常见的三个坑:噪声、错位、读数跳变
6.1 噪声大的排查链路
波形显示出来如果是一条布满毛刺的粗线,首先要区分是电源噪声还是前端耦合噪声。
第一步看毛刺的特征。如果是50Hz工频干扰,波形上能看到明显的正弦波叠加,而且频率固定,这时重点检查右腿驱动电极是否贴好、RLD_SENS寄存器是否配置正确。如果右腿驱动没使能,人体成为浮空天线,50Hz干扰会非常严重。
第二步把模块的输入引脚直接短接,如果波形变得平坦,说明噪声来源在导联线和电极接触部分,可能是导联线质量问题或屏蔽层破损。如果短接后波形仍然毛躁,说明噪声来自模块供电或数字地干扰,集中在电源纹波上。
第三步用示波器看模块供电电压的纹波。纹波峰峰值超过20mV时,对心电数据的影响就很明显了。解决办法是把ADS1292模块的电源独立出来,用低噪声LDO供电,并且在模块供电引脚附近并联一个10uF电解电容和一个100nF陶瓷电容。
6.2 数据错位的根因定位
串口助手上波形如果出现周期性跳变,最典型的特征是波形在某一时刻突然有很大幅度的上下跳动,然后又恢复正常。这种问题常见根源有三个:SPI时序配置错误、DRDY引脚处理不当、CS片选时序问题。
时序配置检查方法如下:把采样率降到100SPS,如果跳变频率相应降低,那么问题大概率在SPI读取的字节对齐上。ADS1292每次读取必须一次取完6个字节的通道数据,中途不能插入其他SPI操作,否则后续数据就会错位。
CS片选时序是很多人忽略的细节。每次操作前拉低CS,操作结束后必须拉高CS,CS拉高期间芯片会结束当前操作。如果CS一直保持低电平,芯片的状态机和SPI通信时序可能会混乱。
还有一个隐蔽的坑是DRDY和CS引脚的复用冲突。如果DRDY引脚和CS引脚不小心连到了同一个GPIO,读数据时每次拉低CS都会误触发DRDY的等待条件,导致死循环或者数据错乱。接线时千万检查这两个引脚有没有正确区分。
6.3 心率读数跳变的处理
心率读数跳变,通常是R波检测算法阈值不合适或者检测到了T波。T波在某些导联下幅度较高,如果阈值设置太低,可能把T波当成R波,导致心率直接翻倍。
处理T波误检的办法之一是增加不应期。检测到一次R波后,在后续200ms内不再接受新的R波脉冲,因为正常人R波之间至少间隔300ms(对应200bpm),200ms的不应期不会漏掉真实心跳,但能有效过滤掉紧跟在R波后面的高频噪声。
另一个改进是把差分阈值和幅度阈值结合起来。R波除了斜率大,其峰峰值也明显高于T波。可以同时要求“检测到的脉冲幅度超过设定的最小幅度”才认定为有效R波。不过这个最小幅度也应该是动态的,否则会因为电极接触变化而产生漏检。
最后提醒一个非常实际的问题:心率算法验证阶段,别急着用自己身体测试,容易受呼吸和肌电影响。先用ADS1292的内部测试信号做算法验证,调通之后再接人体。如果你用测试信号调算法,可以先用一个已知频率的正弦波输入,确认检测到的频率和设置的一致,再切换到自己身上测试。
我在实际调试中还有一个体会:SPI时钟速率尽量先从慢开始,比如1MHz,确认通信稳定后再逐步提高。很多人一上来就用最高速率,结果偶然出现数据错位却很难排查。先慢后快,最终稳定在一个可靠速率上,整个过程会顺畅很多。如果这个项目跑通了,后续可以往两个方向扩展:一是把存储加进来,加一个SD卡或者Flash,做成简易的心电记录仪;二是把通信方式换成蓝牙,把数据发到手机端显示,那就从“串口助手上看波形”升级成为一个小型可穿戴设备原型了。
本文还有配套的精品资源,点击获取