在实验室折腾了两个月,我终于把手头这块绿油油的PCB板子做成了能稳定工作的蓝牙心电监护仪。这不是什么高精尖设备,核心物料成本不到三百块,但它真正解决了一类问题:当你想在运动、睡眠或者日常通勤中连续记录心电波形的时候,医院里那台插着电线的监护仪不可能跟着你跑。做一个带蓝牙的心电采集设备,让实时波形直接出现在手机屏幕上,这个念头我琢磨了很久,也踩了无数个坑,今天把它完整地整理出来。
这个项目的本质,是把一条完整的生物电信号链装进一个小盒子里。电极把人体的微弱电信号捡起来,经过模拟前端放大和滤波,交给ADC转换成数字量,再通过蓝牙送到手机端实时显示。适合谁看?如果你对模拟电路、嵌入式开发、BLE协议栈或者简单的医疗电子DIY感兴趣,这篇内容应该能提供一条清晰可复现的路径。我会把选型理由、参数计算、协议设计和实际踩坑都写出来,不想只给结论,想把"为什么"一起讲透。
1. 重新定义需求:不是"做出心电波形"而是"做出一台能带着跑的设备"
1.1 从医院的十二导联到口袋里的单导联
正经的临床心电图机是十二导联体系,十个电极贴在胸前和四肢上,记录的是心脏不同方向的电活动。这个方案放在病房没毛病,但你想做成随身设备就不现实了,没人愿意在身上贴十片电极再拖着一捆线跑一天。
所以项目第一条边界就是:做单导联。三条线,最多三片电极,一条接右胸或左胸,一条接另一侧,第三条是右腿驱动(RLD)地线。单导联看心率、看P波、看QRS波群、看T波,监测心律失常的粗筛完全够用。真要诊断心肌缺血定位这类问题,那是医院十二导联的事,不是DIY设备该揽的活。明确这个边界,后续所有设计都能聚焦。
这个项目真正要回答的问题其实是三个:信号能不能采得干净,数据能不能传得及时,波形能不能看得清楚。围绕这三件事,需求拆解成下面几条:
- 输入范围能覆盖0.05Hz到100Hz的心电信号,增益足够让1mV量级的信号清晰可见
- 连续工作时间至少8小时,所以必须低功耗
- 手机能实时显示波形,数据不卡顿、不严重丢包
- 体积控制在烟盒大小以内,方便绑在手臂或贴在胸前
1.2 蓝牙是唯一合适的无线方案?对比了一圈
无线方案看起来选择很多,真限制下来其实没几个。2.4G私有协议比如nRF24L01,速率和功耗都不错,但手机端没法直接接收,必须配一个USB接收器,这等于给用户增加了一个必须带着的"尾巴",Pass。WiFi功耗太高,用电池扛不住连续几小时传输,而且还需要路由器或者手机开热点,Pass。剩下就是蓝牙。
蓝牙内部还能再分。经典蓝牙SPP,也就是HC-05那种模块,特点是吞吐率大、实现简单、Android对SPP支持很好,但两个硬伤:功耗高,而且iOS系统根本不支持SPP。如果你的目标是安卓手机,那HC-05是个偷懒的好选择;如果你希望这个设备未来能在iPhone上打开蓝牙就能连,那就只能走BLE。
BLE功耗低,手机原生支持,而且现在的BLE芯片已经不缺算力。唯一的顾虑是吞吐率:BLE 4.0一个连接间隔能传的有效数据有限,后来BLE 4.2和5.0加入了DLE(Data Length Extension)把单包数据量从20字节提到251字节,彻底解决了带宽问题。我用的是nRF52832模组,支持BLE 5.0,这个选择是冲着功耗和封装尺寸去的。结论很明确:走BLE,别回头。
2. 信号链硬件设计:毫伏级信号如何安全走到ADC
2.1 AD8232:集成模拟前端为什么省事
心电信号的脾气很多人第一次接触时都会懵。幅度只有0.5到5毫伏,频率范围主要在0.05到100赫兹,周围还有随处可见的50赫兹工频干扰、肌电干扰、电极极化电压。你总不能自己拿一堆分立运放从零搭一个仪表放大器再加滤波器,那调试周期会让你崩溃。
AD8232几乎就是为单导联心电采集量身定做的模拟前端芯片。它内部集成了一个仪表放大器(增益由外部电阻设定,我设成100)、一个右腿驱动放大器、一个高通滤波器、一个低通滤波器,还有导联脱落检测功能。外部只需要少量RC元件就能把信号带宽限制在0.5Hz到40Hz,这个带宽正好保留了PQRST波群的形态,又能甩掉大部分高频噪声。
为什么我的增益只设到100?这是有计算依据的。假设ADC参考电压是3.3V,心电信号峰值大约1mV,放大100倍后是100mV,只占了3%的ADC量程。如果用的是12位ADC,这相当于4096个码值中的约124个码值,能分辨但确实不够细腻。所以AD8232后面最好再接一级额外的放大,或者直接选择更高分辨率的ADC。我在原型阶段先接了Arduino的10位ADC,虽然能出波形但量化噪声明显;后来换成了24位的ADS1292R,立刻发现基线平滑得多,这也是我推荐ADS1292R的原因。
ADS1292R是另一套更"大而全"的方案,内部集成了两个24位Delta-Sigma ADC、可编程增益放大器、右腿驱动以及呼吸检测模块,而且支持SPI接口直接读数字量。用这颗芯片的话,模拟前端几乎不需要外部电路,固件配置寄存器之后就直接出24位数据。它和AD8232的本质区别在于:AD8232输出的是模拟电压,还需要外接ADC;ADS1292R把采集和数字化一次做完,噪声和集成度都更好。低成本原型用AD8232加单片机自带ADC,要求波形质量就上ADS1292R,这是选型的基本思路。
2.2 电源和接地:电池供电才是隐藏功臣
ECG电路对电源纹波极其敏感。如果用5V的USB电源适配器供电,你会看到波形里多出一条粗粗的粉红色噪带。这不是采集电路的问题,是电源本身带有几十毫伏的开关纹波,直接串入了信号链。
我的做法很传统但很实用:3.7V锂电池加一个低压差线性稳压器(LDO)降到3.3V。线性稳压器没有开关动作,输出纹波可以做到微伏级别,这正是模拟电路需要的"安静"电源。电池选的是400mAh的聚合物锂电池,实测整机功耗大约15mA,算下来能连续工作26小时,一天一充绰绰有余。
还有个容易忽略的点:ADC和蓝牙模组如果共用一个LDO,射频发射瞬间会拉低电压,在模拟信号里看到毛刺。我把模拟部分(AD8232或ADS1292R)、数字部分(MCU)、射频部分(BLE模组)各用一颗LDO分开供电,模拟地和数字地在PCB的单点位置汇合,这个细节直接决定了波形底噪能不能压到0.1mV以下。
2.3 电极、导联线与屏蔽
电极是很多人不喜欢提但绕不开的话题。医用Ag/AgCl电极湿式凝胶电极接触阻抗低、信号质量最好,但凝胶会干,不适合长时间佩戴。干电极或者布电极舒服,但运动的时候接触阻抗变化很大会引入运动伪迹。我实测下来的经验是:静态测量或者睡眠监测用湿式电极,运动场景还是先用胸带式方案更靠谱。
导联线一定要用屏蔽线,而且屏蔽层只能单端接地。如果两端都接地,会形成地环路,50Hz工频干扰反而更严重。线的长度尽量短,因为人体信号本身只有毫伏级别,线缆一旦超过50厘米,即使有屏蔽也能捕捉到环境噪声。我最终把导联线控制在15厘米,勉强够从胸口延伸到腰带上的盒子。
3. 蓝牙链路设计:每秒几千字节怎么稳定装进小包
3.1 数据量核算:先算账再选协议
很多人做BLE项目死在第一步,数据怎么传都传不完,他们通常没做带宽核算。我们来算一下ECG的账:
采用ADS1292R,24位ADC,采样率250Hz。理论上每秒数据量是250 × 3 = 750字节,换算成比特流是6000bps。BLE 4.0单包最大20字节,所以每秒至少需要37.5包。听起来不多,但别忘了协议封装、CRC校验、包头开销,实际每包能装的有效数据可能只有16到18字节。在BLE连接间隔15毫秒的配置下,理论上每秒钟最多约66个连接事件,每个事件最多传输3包,也就是约200包每秒的极限能力,37.5包的负载其实完全装得下。
问题在于手机端的BLE协议栈调度不一定稳定,实际环境里包与包之间会有丢包和重试,很快吃掉富余量。所以我把24位数据压缩成16位传输:ECG的20Hz带宽信号实际上有效分辨率做到16位已经非常干净,人眼根本分辨不出24位和16位显示在手机屏幕上的区别。这样每秒数据量变成500字节,只需要25包每秒,余量就非常充裕了。这个取舍看似损失精度,实则是工程上最明智的一笔。
3.2 自定义分包协议:帧头、序号、CRC一个都不能少
BLE是串行流式传输,接收端必须自己还原出"这是一帧心电采样数据"这件事。我定义了一个非常简单的协议帧,总共20字节,正好塞满BLE 4.0时代的一个包:
typedef struct __attribute__((packed)) { uint8_t header; // 固定 0xAA uint8_t version; // 协议版本 0x01 uint8_t seq; // 包序号,0~255循环 uint8_t flags; // 标志位,如电池低电量、导联脱落 int16_t samples[8]; // 8个连续采样点,每个16位 uint8_t crc8; // 对前19字节的CRC8校验 } ecg_packet_t;帧头0xAA用来做字节同步,接收端在流里找帧头后按固定长度切包。包序号是点睛之笔:上位机发现序号不连续就可以判断发生了丢包,然后决定是插值补点还是打上"数据缺失"标记。以前我没加序号的时候,波形出现断点完全无感知,最后心率计算就莫名其妙多了几个峰。CRC8虽然强度不高,但对BLE传输链路来说,协议栈本身有24位CRC检验,我加这层更多是防止自己代码逻辑里出现字节错位。
3.3 连接参数调优:让BLE吞吐靠近理论值
BLE的一个核心概念是"连接事件"(connection event):主从设备约定每隔一段时间交换一次数据。如果你保持默认的连接参数,很多BLE芯片初始连接间隔是30毫秒甚至50毫秒,真正能发送的包数量会非常少。我踩过一次坑,连接间隔默认45毫秒的时候,手机上波形图一卡一卡的,像PPT动画。
在固件里主动请求合适的连接参数是必须的。关键参数有三个:
- 连接间隔(Connection Interval):越大越省电,越小吞吐越高。我的项目设成7.5毫秒,这是BLE规范允许的最小值
- 从机延迟(Slave Latency):允许从设备跳过若干个连接事件从而省电。为了吞吐率我把它设为0,即每个连接事件都必须参与收发
- 数据长度扩展(DLE):BLE 5.0芯片可以把单包ATT数据提升到251字节。虽然我的帧只有20字节,但开启DLE能减少链路层分包开销
实际测试下来,连接间隔7.5毫秒加从机延迟0,实际吞吐大约每秒150包左右,扣除协议开销后有效数据率约2500字节每秒,远超500字节每秒的需求。但我必须提醒一句:功耗和吞吐是矛盾的。连接间隔越小,射频唤醒次数越频繁,功耗相应升高。经过几天测试,我的应用中平均电流15毫安仍然可以接受,如果你做的是需要一颗纽扣电池撑一个月的穿戴设备,这个参数就得重新权衡。
3.4 调试利器:手机上的串口蓝牙终端
固件刚写完那会儿,我还没做App,怎么验证蓝牙链路通不通?这里要强烈推荐一个调试工具:Android手机上的Serial Bluetooth Terminal,中文叫串口蓝牙终端。这个App能直接以SPP或BLE方式连接从设备,然后以十六进制或ASCII方式显示收到的原始数据。我用它来验证协议帧的字节结构,快速确认帧头、长度、CRC是否正确。
串口蓝牙终端的价值在于,它把你的手机变成一个无线串口调试器,不用先写一行App代码就能看到底层数据。等协议确认无误再动手写图形界面,能省掉至少一周的联调时间。另一个配套工具是nRF Connect,Nordic官方出的,可以查看BLE服务的特征值、通知值,也能直接手动读写。我把这两个工具推荐给所有做BLE项目的朋友,它们顶得上半个高级工程师。
4. 固件架构与实时性:从裸机到状态机
4.1 采集发送流水线:中断、FIFO与主循环
固件的核心设计可以抽象成一条流水线:定时器精确产生250Hz采样中断,中断服务函数里从ADS1292R读取最新一次采样结果,写入一个环形缓冲区;主循环不断检测缓冲区是否有数据,有就按8个样本一包取出,加上协议头,通过BLE发送出去。
这里最关键的是FIFO缓冲区的设计。ADS1292R的SPI读取很快,几十微秒就能搞定,但BLE发送不是即时的,它要等主设备和从设备的下一个连接事件。如果中间没有任何缓冲,采样中断一旦发生在发送间隙就会丢数据。我分配了256个样本的环形缓冲区,能缓存大约1秒的数据。当BLE因为某种原因卡住时,这一秒钟的缓冲能保护数据不丢。缓冲区快满时我还会把采样率临时降一档,这是软件上的自动避让。
状态机拆分得很简单,就三个状态:待机、已连接、传输中。待机状态下BLE广播但不发数据;手机连接上之后进入已连接,此时先做一次导联脱落检测和电池电压检测,再开始发数据;传输过程中如果BLE断开,自动回退到待机并重新广播。这个状态机代码量不大,但让整个设备行为非常清晰。
4.2 丢包、断连和重连策略:别让设备变成砖头
BLE在复杂射频环境中丢包是常态,不是末态。协议栈本身有重传机制,但如果连续多次传输失败导致连接超时,设备会静默断开。我做了一个策略:连续5秒没有成功发送任何一帧数据,就主动从连接状态回到待机状态,然后重新开始广播。因为手机端App通常有自动重连功能,如果设备一直停在"半连接"状态而不主动断,App反而不知道该怎么处理。
另外要特别提醒的是BLE的广播参数。广播间隔太短会迅速耗尽电量,太长则导致手机搜索不到。我实测广播间隔100毫秒是一个不错的中点,手机端大约2秒内能搜到,功耗也在可控范围。如果你发现手机上经常搜不到设备,先把广播间隔调到50毫秒试试,确认连接稳定后再逐渐调大。
5. 上位机与实测结果:波形出现在屏幕上才算数
5.1 手机端实时绘制的取舍
手机端App我用Flutter写了个跨平台壳子,蓝牙库用flutter_blue_plus,画波形用CustomPaint直接操作Canvas。为什么不用现成的图表库?因为实时波形刷新率很高,通用图表库的动画和缓动机制会拖慢刷新速度,CustomPaint自己控制每个数据点的绘制反而最直接。
绘制逻辑需要注意一个事情:不要在UI线程里解包BLE回调数据。我把BLE收到的原始包丢进一个队列,由单独的DrawThread每40毫秒取一次数据,把最近2秒的波形画到屏幕上。这样做UI不会因为数据解析卡顿,刷新率稳定在25帧左右,肉眼看上去波形非常平滑。
5.2 实测波形与常见问题:基线漂移、工频干扰与丢包断裂
把电极贴在胸口,打开App,第一次看到自己心电波形那一刻是很有成就感的。P波、QRS波群、T波清晰可辨,心率63bpm,和医用心电图机对比过,波形形态基本一致。
但我很快遇到了三个问题。第一个是基线漂移:身体稍微动一下,波形整体就跑到屏幕外了。原因在于电极接触阻抗变化产生了低频伪迹。解决办法有几个层面:硬件上加高通滤波器截止频率调到0.5Hz以上,软件上做滑动平均基线扣除。第二个是50Hz工频干扰,波形上叠加了一层细密毛刺。原因是右腿驱动电极接触不良,重新贴好电极后毛刺立刻消失;另外在软件上我顺手加了一个50Hz整系数陷波器,能进一步压低残余干扰。第三个是蓝牙丢包导致波形断裂。
丢包断裂的排查过程让我印象很深刻。一开始我怀疑是BLE吞吐不够,但算过数据量之后不该不够。后来用nRF Connect抓无线日志才发现,问题出在手机端的蓝牙调度上:某些国产手机在锁屏状态下会大幅降低蓝牙射频活动频率,导致连接事件不足。解决方案是App层保持屏幕常亮,固件层增大FIFO缓存,双管齐下后丢包率从3%降到了0.2%以下。
5.3 Windows驱动与串口联调的暗坑
工程调试阶段我在Windows笔记本上接了一个BLED112蓝牙适配器,想用PC端的串口工具直接看数据。结果插上适配器后,系统自己装了一个"Generic Bluetooth Radio"驱动,然后设备管理器里完全找不到虚拟串口。这个现象很典型,处理办法是:
- 先把系统自带的Generic Bluetooth Radio驱动禁用,让设备使用厂商驱动
- 手动安装Bluegiga提供的驱动包,装好后会出现虚拟COM口
- 此时PC端串口工具就能像操作普通串口一样操作BLE设备
如果你在Windows上拿HC-05之类经典蓝牙模块做SPP调试,也会遇到类似问题:配对成功但COM口不出现。常见的原因是Windows默认把这个设备识别成了键盘或者音频设备,而不是串口设备。我后来学到的经验是:给HC-05 AT指令集里设置好设备名和服务UUID,确保系统把它识别为COM口设备,比在驱动管理里折腾半天可靠一百倍。
6. 还能往哪儿走:从单导联到医疗级之间的距离
6.1 HRV分析与运动场景扩展
把波形显示出来只是一个起点。单导联ECG数据里藏着一个非常有价值的指标:心率变异性(HRV),也就是相邻两次心跳的时间间隔变化。这个指标和自主神经功能、压力状态、疲劳程度都有相关性。我在固件里加了一个简单的R波检测算法:对波形做一阶差分,超过阈值且间隔大于400毫秒的峰视为R波,然后从App端记录每个RR间期并计算SDNN。这个功能放在运动记录场景里特别有意思:配合手机GPS记录位置,你就知道在哪个坡段心率飙升、恢复期花了多久。GPS和ECG的数据都有时间戳,但两者时钟不同步会导致对齐困难,我的解决办法是GPS事件也打上BLE数据包序号作为时间基准,这样回放时能把位置和心电图精确对齐到单个采样点。
6.2 多导联与穿戴化:下一步的技术路线
单导联验证了信号链的可行性,接下来可以考虑两个方向。一是多导联:ADS1292R本身有两个通道,可以同时采集两条导联,比如I导联和II导联,能提供更多心电轴信息。但多导联意味着数据量翻倍,BLE带宽又变成瓶颈,这时候开启DLE扩展、把有效数据包从20字节提到244字节是必须的。二是穿戴化:PCB已经能做到2厘米乘3厘米,和一片硬币差不多大,下一步把它嵌到一个弹性织物胸带里,电极直接用织布电极,就能变成真正的运动心率衣。别低估穿戴化的工程量——柔性电极、防水、动态噪声抑制,每一项都是独立课题。
6.3 关于医疗级认证与数据安全的建议
我必须诚实地说一句:这篇文章做出来的设备是学习研究和运动监测级别的,不能作为临床诊断依据。医疗级ECG设备需要满足的是IEC 60601-2-25标准,涉及漏电流限制(必须小于10微安)、电击防护、暗含的信号真实性等大量认证工作。DIY设备连最基本的医用级电气隔离都不一定有,用它来判断自己是不是心律失常,风险极高。
如果这个项目未来真的往产品方向走,有两件事要尽早规划。一是数据隐私:心电数据属于敏感个人健康信息,传输需要加密,存储需要合规,至少在手机端做好访问控制,云端存储的话一定要按当地法规走。二是和医疗机构合作做临床验证,这不是可有可无的流程,而是真正能说明设备性能的硬证据。
我在这个项目里最深的体会是:电路和代码只是表面功夫,真正的功夫在信号链的理解和系统级的取舍。每当我看到一个漂亮的P波在手机屏幕上跳出来,都觉得之前那些趴在示波器前查噪声的夜晚没有白费。这个项目看起来不难,但它的意义在于把几门学科缝在了一块板子上。如果你也想动手做一套,记住我的建议:先调到能稳定看到清晰的QRS波群,再去追求花哨的功能。波形干净了,一切才有讨论的基础。