做了不少电子 DIY 项目之后,我越来越觉得心率计这东西属于“看着简单,做起来全是细节”的典型代表。市面上几十块钱就能买到腕式心率计,但自己用一块 51 单片机、一个光电传感器、一块 LCD 屏和几个按键,从头搭一套能测脉搏、能显示心率、还能超限报警的成品装置,这中间要踩的坑远比想象中多。这篇文章就把我做这套“基于51单片机心率脉搏检测仪”的思路、电路设计、信号处理流程、软件算法和实测校准过程完整梳理一遍。如果你正准备做类似的课程设计、毕业设计,或者只是手痒想做一个能戴在手上的小玩意,这篇内容应该能帮你省下大量试错时间。
先说清楚这套设备最后能做到什么程度:它可以实时采集指尖脉搏信号,通过算法计算当前心率并在 1602 液晶屏上显示,支持设置心率上下限,当测量值超出设定范围时,蜂鸣器报警、LED 闪烁提醒。整机用的主控是很常见的 STC89C52 这类 51 内核单片机,成本可控,电路也不会复杂到劝退新手,但它的信号调理链路、软件滤波和报警判断逻辑,完全是按“能稳定工作”的成品的标准去做的。
1. 项目定位:51单片机做心率计的合理性在哪
1.1 整机要解决的需求与功能边界
很多人在选型时会纠结一个问题:为什么不用更高级的 ARM 或者带 DSP 的芯片?答案是没必要。心率检测本质上是一个低频信号处理任务,脉搏波的频谱能量集中在 0.5Hz 到 5Hz 之间,正常人静息心率大概在每分钟 60 到 100 次,换算成频率也就是 1Hz 到 1.67Hz 附近,即使是运动状态下也很少超过 200 次/分钟,也就是约 3.3Hz。这样一个窄带低频信号,51 单片机在资源上是完全够用的。
但“够用”背后有一个前提,就是系统设计必须合理。心率计不是把传感器往手指上一夹就能出数的,它需要经历“物理信号采集 -> 电流转电压 -> 信号放大 -> 滤波整形 -> 脉冲测量 -> 心率计算 -> 显示和报警”这样一整条链路。任何一个环节处理不当,最终都会在心率数值上表现为跳数、乱跳或者干脆没有反应。所以这块 51 项目的重点,不在于主控芯片本身,而在于模拟前端的设计和软件对信号的解读能力。
这套设备的功能边界我把它定成三块:第一,实时检测脉搏并显示心率值;第二,提供心率上下限设置功能;第三,超限时同时触发声音和光报警。往下不扩展到心率变异性分析、血氧饱和度测量这类更复杂的生理参数,往上也不做蓝牙联网、手机 App 同步,这样能把项目复杂度控制在一个单人单月左右可以完成的范围,也更适合作为教学演示或入门综合项目的载体。
1.2 51单片机上的资源盘点:能省则省的硬件方案
51 单片机家族型号很多,我这次用的是 STC89C52RC,主要看中它 8KB Flash、512 字节 RAM,两个定时器、一个串口,以及足够的 GPIO。对于心率计来说,真正的硬资源需求只有三个:一个能够捕获外部脉冲沿的中断引脚,一个能够精确计时的定时器,以及足够驱动液晶屏和报警器的 I/O 口。
这里插一句为什么不做 ADC 采样方案。传统 51 比如 AT89C52 内部没有 ADC,即使 STC89C52 系列的部分型号也没有内置 ADC 外设,想要采集模拟脉搏波形就必须外挂 ADC0809 或者串行 ADC 芯片,这会让电路复杂程度和程序调试难度同时上一个台阶。在所有心率计方案里,我更推荐先通过模拟电路把脉搏波信号整形成 TTL 脉冲,再用 51 的外部中断测量脉冲周期,最后换算成心率。这种方案硬件成本低,软件逻辑清晰,而且容易调试。
所以最终的硬件资源分配是这样:P3.2 用作外部中断 0 的输入引脚,接收整形后的脉搏脉冲信号;定时器 0 工作在方式 1,作为一个 16 位定时器用来记录脉冲之间的时间间隔;P0 口接 1602 液晶屏的数据线,P2.0 到 P2.2 接液晶屏的控制线;P1 口接按键;P2.5 和 P2.6 分别接蜂鸣器和报警指示灯。整机只用了很少的 I/O 资源,这为后续扩展留下了充足余量。
2. 脉搏波信号链:从指尖到TTL脉冲
2.1 光电容积脉搏波原理
心率检测的物理基础是光电容积脉搏波描记法,英文缩写 PPG。这个原理说穿了很简单:血液在血管里是脉冲式流动的,心脏每收缩一次,血液就会把动脉血管撑大一次,血管内血流量随之增加;而血液对特定波长的光有明显的吸收作用,当光线穿过手指这类组织时,透射光或者反射光的强度就会随着血管搏动发生周期性变化。
具体到我的这套装置,我选用的是红外发射管配合光敏二极管的透射式方案。手指夹在红外 LED 和光电二极管之间,红外光穿透手指后被光电二极管接收。心脏收缩期,手指血容量增加,红外光被吸收得更多,到达光电二极管的光强减弱,输出电压变小;心脏舒张期,血容量减少,输出电压变大。就这样,每一次心跳都会在传感器输出端产生一个微弱的脉动信号,频率与心率完全一致。
这里有一个必须理解的关键点,这类信号有多微弱。光电二极管输出的电流变化量通常在微安级别,甚至更小,而且叠加在一个比较大的直流分量上。直流分量是手指组织、骨骼、静脉血等静态部分对光的吸收产生的,我们关心的脉动分量可能只占整体信号的 1% 到 2%。如果直接把传感器输出接到单片机上,几乎看不出任何波形变化,所以后面必须配套放大和滤波电路。
2.2 放大、滤波、比较三级电路设计
信号调理电路是整个项目的灵魂,我把这一块分成三级。第一级是电流转电压和初步放大,用了一个运放组成跨阻放大器,把光电二极管的电流信号转换成电压信号,同时做第一级放大;第二级是电压放大,把第一级输出的微弱电压继续放大到适合处理的幅度;第三级是有源带通滤波,滤除直流分量和高频噪声,让脉搏波信号变得干净。
在具体参数选择上,第一级跨阻放大器的反馈电阻我选了 1M 欧姆,这样 1 微安的电流变化可以转换成约 1V 的电压变化。第二级同相放大器的增益设置在 20 倍左右,两级合起来的增益大约 100 多倍,已经足够让脉搏波信号达到伏特级。第三级带通滤波器的高通截止频率设在 0.5Hz 左右,用于去掉基线漂移和直流分量;低通截止频率设在 5Hz 左右,用于滤除呼吸干扰、肌肉抖动和 50Hz 工频干扰。这个通带范围正好覆盖心率信号所在的频段。
滤波器我用的是二阶有源带通结构,两个运放就能实现。实际调试时要注意,运放的正负供电必须对称稳定,如果出现波形上下不对称或者信号漂移严重,优先检查运放电源有没有做好去耦。这一级的电容选择也有讲究,滤波电容最好使用 CBB 电容或者精度较高的薄膜电容,电解电容在低频段的漏电流较大,可能会导致滤波器中心频率偏差。
2.3 比较器方案与ADC方案的取舍
信号经过放大滤波之后,得到的是一个伏特级的模拟脉搏波形,此时面临两个选择:一个是直接用 ADC 采样波形,再在软件里通过算法识别峰值并计算心率;另一个是用比较器把波形整形成方波脉冲,再用单片机测量脉冲周期。
我最终选了比较器方案。原因是这个方案对单片机的资源占用最少,实时性最强。比较器我把参考电压设在脉搏波形幅度的中间位置附近,也就是触发电平大约 1.5V,当信号电压高于参考电平时输出高电平,低于参考电平时输出低电平。这样每一次心跳都会产生一个完整的方波脉冲,单片机只需测量两个脉冲之间的上升沿间隔,就能得到一次心跳的周期,再用 60 除以周期得出心率值。
但比较器方案有一个致命的坑,就是容易产生振铃和抖动。当信号缓慢穿越参考电平时,比较器输出会在临界点附近反复跳变,产生一串毛刺,最终导致心率乱跳。解决办法是在比较器上加一点正反馈,也就是施密特触发器形式的滞回比较。我给比较器加了一个 100K 欧姆的正反馈电阻,制造出大约几十毫伏的滞回窗口,这样虽然牺牲了极小一部分边缘灵敏度,但换来的却是脉冲边沿的稳定可靠,实测下来效果非常明显。
3. 硬件组装与信号完整性
3.1 模块连接与引脚分配
硬件连接上,我把系统分成了五个模块:传感器模块、信号调理模块、主控模块、显示模块和报警模块。传感器模块输出连接到调理电路输入,调理电路的比较器输出连接到主控的外部中断 0 引脚,主控的 P0 口连接 1602 液晶屏数据线,P2.0 到 P2.2 连接液晶屏控制线,P1 口连接四个按键,P2.5 和 P2.6 分别连接蜂鸣器和报警灯。
具体引脚分配我列在下面表格里,方便照着实测搭建:
| 主控引脚 | 连接对象 | 说明 |
|---|---|---|
| P3.2 / INT0 | 比较器输出 | 脉搏脉冲输入,下降沿触发外部中断 |
| P0.0 - P0.7 | 1602液晶屏D0-D7 | 数据总线 |
| P2.0 | 1602液晶屏RS | 寄存器选择 |
| P2.1 | 1602液晶屏RW | 读写选择 |
| P2.2 | 1602液晶屏EN | 使能信号 |
| P1.0 - P1.3 | 按键 | 菜单/加/减/确认 |
| P2.5 | 蜂鸣器驱动 | 高电平触发 |
| P2.6 | 报警指示灯 | 高电平点亮 |
这里要注意 1602 液晶屏的背景光引脚不能直接接满 5V,否则容易烧灯,串一个 100 欧姆左右的限流电阻比较保险。蜂鸣器如果是无源蜂鸣器,建议用 NPN 三极管驱动,因为单片机 I/O 口的驱动能力有限,直接驱动蜂鸣器可能会出现音量和启动电流不足的问题,这在成品设计中是一个常见却又容易被忽略的细节。
3.2 电源、地线、屏蔽的经验
很多 DIY 心率计做出来之后显示的数据乱跳,问题往往不在算法,而在电源和地线。脉搏信号是微伏到毫伏级别的微弱信号,而单片机数字电路工作时的开关噪声、液晶屏刷新时的电流突变、蜂鸣器发声时的瞬间大电流,都会通过电源线和地线传导到模拟前端,造成信号干扰。
我在这个项目里做了三件针对性的事。第一,模拟电路和数字电路尽量分区布局,传感器、运放、比较器这些小信号器件集中放在板子的一侧,单片机、液晶屏、蜂鸣器放在另一侧,中间用地线进行分割;第二,运放电源引脚旁边加了一个 10uF 电解电容和一个 104 瓷片电容做去耦,这两个电容一个滤低频一个滤高频,配合使用效果最好;第三,蜂鸣器的电源和地线单独走线,不要和传感器电路共用过长的公共地线,避免蜂鸣器动作时产生的电流冲击影响到脉搏信号的处理。
传感器探头部分我用了带屏蔽壳的光电模块,只留出一个透光窗口。这个封装细节很重要,因为环境光尤其是日光灯的高频闪烁会直接叠加到脉搏信号上,完整的遮光处理能挡掉大部分环境光干扰,让信号的基线稳定很多。如果条件允许,把整个模拟信号链包在一个金属屏蔽罩里效果更佳,不过手工搭建时用绝缘胶布加黑色热缩管也能达到差不多的效果。
3.3 关键器件选择避坑
心率计项目里器件选型有一些不太容易注意到的点,这里单独拎出来说。红外发射管选 940nm 波长的,这个波段不容易被可见光干扰,更适合透射式测量;光电二极管选和发射管波长匹配的型号,响应峰值在 940nm 附近最佳,这样信号强度才能最大化。
运放我用的是 LM358,双运放封装,一颗就能完成跨阻放大和电压放大,滤液工作由第二颗完成,板上总共两颗 LM358 就够了。它的优点是便宜、货源充足、单电源也能工作,缺点是带宽不大、噪声稍高,但对 5Hz 以下的脉搏信号来说完全够用。比较器选 LM393,同样便宜易得,开漏输出(在有上拉电阻)也可以。
有几个型号参数我必须提醒:运放不要选太高速的型号,高速运放在这种低频微弱信号场景里反而容易引入额外的噪声和自激振荡;光电二极管选响应面积稍大一点的型号,这样手指放置位置的容错性更好,但如果引脚孔径不一致,权衡一下选择的是 TO-18 封装和 TO-5 封装的差别,差别在实际受光面积和灵敏度上。这些都不是说明书里会写的参数,但实际焊接和测试一圈下来,影响非常直接。
4. 软件核心:测量间隔、计算心率、触发报警
4.1 定时器中断与脉冲间隔测量
硬件上把脉搏信号变成 TTL 脉冲之后,软件要做的事情就是精确测量相邻两个脉冲之间的时间。我选择用外部中断 0 下降沿触发,每次触发时去读取定时器 0 的当前计数值,然后和上次触发时的计数值做差,差值就是本次心跳周期。
但这里存在一个 16 位定时器不够用的问题。51 单片机的定时器 0 在方式 1 下是 16 位计数器,最大计数值是 65535。我用 12MHz 晶振时,定时器每条计数约 1 微秒,65535 微秒也就是约 65 毫秒。而正常心率的周期在 300 毫秒到 1500 毫秒之间,远大于 65 毫秒,所以定时器会溢出多次。解决这个问题的方法是配合一个软件溢出计数变量,每次定时器溢出中断时给这个变量加 1,在测量脉冲间隔时同时读取溢出计数器和 16 位计数器,合成一个 24 位甚至 32 位的时间戳。
关键代码片段如下,这里以 C51 为例:
unsigned int th_val, tl_val; unsigned char ov_cnt; unsigned long now_tick, last_tick, interval_tick; void timer0_isr(void) interrupt 1 { ov_cnt++; // 每次溢出时加1,等效扩展时间戳高位 } void ext0_isr(void) interrupt 0 { // 读取当前时间戳,先读到计数器和溢出计数值 th_val = TH0; tl_val = TL0; unsigned char ov_tmp = ov_cnt; now_tick = (unsigned long)ov_tmp << 16; now_tick |= (th_val << 8) | tl_val; interval_tick = now_tick - last_tick; last_tick = now_tick; // interval_tick 即两次心跳之间的定时器计数间隔 // 根据晶振频率换算成秒即可得到周期,再算心率 }每次边沿到来时,先读 TH0 和 TL0,再读溢出计数值,通过三者合成一个当前时间戳,然后与上次边沿的时间戳做差,得到的是心跳周期对应的计数个数。因为定时器计数频率等于晶振频率除以 12,所以周期秒数等于计数间隔除以定时器频率,心率就等于 60 除以周期秒数。这个时间戳合成方法操作起来不复杂,但非常稳健,是所有基于 51 定时器测量频率类项目的通用思路。
4.2 平滑滤波与异常值剔除
直接由脉冲间隔换算出来的心率值通常会伴随明显的跳变,原因有两个:一是手指压力不均匀导致某个脉冲幅度过低或过高,比较器没有正常触发;二是偶尔的身体活动或传感器位移产生了额外的干扰脉冲。如果把这些异常值直接显示在屏幕,会看到心率数值在真实值附近不规则跳动,体验相当差。
我做了两级软件处理。第一级是异常值剔除,设定一个心率合理范围,通常取 40 到 220 次/分钟之间。换算成周期就是 0.27 秒到 1.5 秒。当测量的脉冲间隔落在这个范围之外时,认为是干扰产生的假脉冲,直接丢弃,不参与心率计算。第二级是滑动平均,我维护一个长度为 5 的环形缓冲区,每次计算出来的有效心率值都存入缓冲区,然后取这 5 个值的平均值作为最终显示值。5 次滑动平均的时间延迟约为 4 到 5 秒,对于心率监测来说接受度很高,既能有效平滑跳变,又不会让响应慢到影响使用体验。
有的项目还会加入中值滤波器,把窗口内排序之后取中间值,去除单点异常值的能力更强,但对脉搏信号这种本身比较规律的波形来说,异常值剔除配合滑动平均已经足够,不需要额外增加复杂度。
4.3 报警阈值的成品化设计
报警是这个项目的核心卖点,具体逻辑我设计成这样:整机默认的高报警阈值为 120 次/分钟,低报警阈值为 50 次/分钟,用户可以通过四个按键进入设置菜单,分别调节高、低阈值,调整步进为 5 次/分钟,调整结果保存到外部 EEPROM 芯片中,断电后不丢失。
实时监测主循环中,每次更新完心率值后做一次阈值比较,逻辑非常简单:
if (heart_rate >= high_threshold || (heart_rate <= low_threshold && heart_rate > 20)) { alarm_en = 1; // 开启报警 } else { alarm_en = 0; } if (alarm_en) { buzzer_on(); // 蜂鸣器发声 led_on(); // 指示灯点亮 } else { buzzer_off(); led_off(); }报警输出上,我没有让蜂鸣器一直响,而是设计成间断鸣叫模式。蜂鸣器每秒钟鸣叫两次,每次持续 0.2 秒,指示灯同步闪烁,这样既能引起注意,又不至于因为长时间刺耳报警而让人烦躁。这里还有一个人为设计的细节:当心率低于 20 次/分钟时,大概率是传感器没有放置好或者信号丢失,而不是人的心跳真的只有 20 次,所以我不触发“过低心率”报警,而是把它视为信号异常,屏幕上显示“- -”字符,避免误导。这个细节在成品化设计里很重要,能过滤掉很大一部分误报。
在实际测量中,动态范围的调节能力也很关键。由于不同人的手指厚度、肤色、血液含氧量不同,红外光穿透能力差异很大,导致脉搏信号幅度差异明显。为了保证比较器方案适用于更多人群,我在模拟前端加了一个可调电位器,用于调节灵敏度阈值,相当于给信号链路增加了一个手动增益补偿环节。这个设计让整台设备在大多数测试者身上都能稳定测出脉搏。
5. 实测、校准与典型故障排查
5.1 静息状态基准校准
设备焊接完成、程序烧录正常后,不能直接就说“能用了”,必须经历一个校准过程。校准的目标是让系统测出来的心率值尽量接近真实心率,我把这个过程拆成三步。
第一步是静态波形检查。用示波器观察比较器输出引脚的波形,正常情况下应该能看到频率稳定、占空比约 50% 的方波。如果没有方波,顺着信号链一级一级查,先看运放输出有没有脉搏波形,再看比较器输出有没有翻转。第二步是数值校准。找到测试者自己同时测量脉搏,或者用另一台公认准确的心率计作为参照,将两台设备测得的心率值进行对比,误差通常应该在上下 3 次/分钟范围内,如果超出这个范围,优先检查定时器时钟是否准确,也就是晶振频率有没有偏差。
第三步是阈值边界校准。把高报警阈值设置到当前心率附近,确认报警功能能够可靠触发并自动恢复;再把低报警阈值设置到当前心率附近,同样验证边界情况。这一步虽然简单,但能暴露不少隐患,比如 EEPROM 读写异常、按键消抖不到位导致的阈值误修改、报警逻辑里的边界条件错误等。
5.2 干扰定位与排查手段
我实测中遇到过几个比较典型的问题,这里逐个还原出来,方便大家以后排查。
第一种现象:屏幕完全不出心率值,用手挡住传感器比较器输出也没有反应。这种问题绝大多数出在光电传感器或比较器供电上。用万用表查一下红外发射管两端电压,正常应该在 1.2V 到 1.5V 左右;再查比较器的正负输入端电压,如果输入信号一直在参考电压以下,说明前级放大链路有问题。常见原因是运放虚焊或者元件引脚短路,尤其是跨阻放大器的反馈电阻焊接不牢,会导致第一级完全没有输出。
第二种现象:心率值比真实值明显偏高,比如测试者真实心率 80 次/分钟,设备显示 100 多。这种情况通常是噪声被误当成脉搏脉冲,比较器每秒钟翻转了很多次。可以先看波形,如果比较器输出是不规则的毛刺串脉冲,说明信号在穿越参考电平时有反复抖动。解决办法是加大滞回电压窗口,把正反馈电阻阻值调小一点,或者适当提高参考电平静态电平,让比较器翻转更加干脆。
第三种现象:静置时心率稳定,但用户轻轻一动或者手指微动时就乱跳。这是传感器机械稳定性问题,而不是电路问题。手指的轻微移动会改变光在组织中的传播路径,导致信号出现大幅度的基线漂移,进而影响比较器判断。改善的办法是给传感器设计一个夹持结构,让手指在测量时保持相对固定;同时滤除低频的基线漂移,把高通滤波器的截止频率适当提高一些。
5.3 改进方向与实用建议
整套系统做完之后,我盘点了一下还有哪些值得继续打磨的方向,也给你提一些实用建议。
软件算法方面,目前用的滑动平均窗口长度为 5,如果想提高实时性,可以缩短到 3,但数值会更跳动;如果想提高稳定性,可以加长到 8,但响应变慢。实际使用中可以根据具体场景调整,这个参数没有绝对的最优值,只有最适合你的目标状态的取值。
硬件方面,把 1602 液晶屏换成 OLED 屏可以让显示效果上一个档次,代码改动并不大,只需要更换对应的驱动函数,但 OLED 的功耗更低、可视角度更好。如果做完基础版之后还想进阶,可以考虑加一个血氧模块,只需要在现有的光路中增加另一路波长的 LED 并在软件里增加相应的计算逻辑,但需要更复杂的模拟前端。
最后分享一个我在测试过程中印象最深的小技巧。手指传感器每次更换测试者时,都要让测试者先静坐半分钟,等到信号稳定后再读取心率。绝大多数初学者反馈“设备测量不准”,其实不是设备的问题,而是手指刚放上去的那几十秒,末梢血液循环还没有达到稳定状态。这个细节虽然很小,但对测量结果的影响非常大,谁用谁知道。