基于CircuitPython与PPG技术实现心率监测:从硬件原理到信号处理实战

基于CircuitPython与PPG技术实现心率监测:从硬件原理到信号处理实战 1. 项目概述从指尖微光到心跳波形几年前当我第一次拆开一个智能手环看到它背面那颗小小的绿光LED时我就在想这玩意儿是怎么“看见”我的心跳的后来才知道这背后是一项名为光电容积脉搏波Photoplethysmogram简称PPG的技术。它听起来很高深但原理却出奇地直观用特定颜色的光比如绿光照射你的皮肤然后测量有多少光被反射或透射回来。由于血液尤其是含氧血红蛋白对特定波长的光吸收能力很强随着心脏的泵血指尖的血液容积会周期性变化导致反射回来的光强度也发生微弱的同步起伏。捕捉这个起伏就抓住了脉搏的踪迹。这个项目就是带你亲手搭建一个这样的微型“光学听诊器”。我们不需要昂贵的医疗设备主角是一块巴掌大小的Adafruit Circuit Playground Express开发板。它集成了10颗可编程的RGB NeoPixel LED和一个光敏传感器而且两者在板上的物理位置经过巧妙设计正好方便你用手指覆盖。我们将使用CircuitPython——一种对初学者极其友好的嵌入式Python方言来编写代码驱动绿光LED照亮指尖读取光传感器的模拟信号处理数据最终让另一颗LED随着你的心跳闪烁并在电脑上绘制出实时的心跳波形图。这不仅仅是一个简单的“点灯”实验。通过它你将亲手触摸到生物信号处理、模拟信号采集、数字滤波和实时数据可视化的核心概念。虽然它的精度无法与医疗设备媲美但其揭示的原理和实现的流程与市面上许多消费级心率监测设备一脉相承。对于嵌入式开发者、物联网爱好者或是任何对“硬件如何感知生命体征”感到好奇的人来说这都是一次绝佳的实践入门。接下来我将拆解整个过程从原理到代码从硬件连接到数据处理分享我实践中积累的所有细节和避坑经验。2. 核心硬件与原理深度解析2.1 硬件选型为什么是Circuit Playground Express在这个项目中硬件是信号的源头和载体选型直接决定了项目的可行性与体验。我们使用的是Adafruit Circuit Playground Express以下简称CPX这并非随意选择。核心优势在于集成度与设计巧思。CPX在一块圆形的板卡上集成了我们所需的所有关键外设10颗APA102驱动的全彩NeoPixel LED、一个光敏传感器光电晶体管、一个温度传感器、加速度计、麦克风甚至还有触摸电容引脚。更重要的是其工业设计经过了深思熟虑那颗用于发射信号的绿色NeoPixel位于板子边缘标号#1和用于接收信号的光敏传感器旁边有一个“眼睛”图标被紧密地放置在一起。这种布局使得用户只需用指尖自然地覆盖这个区域就能同时挡住光源和传感器形成一个简易的“反射式PPG”检测腔体极大简化了操作。如果使用分离的LED和光敏电阻你需要自己设计光学路径并用不透光的材料包裹防止环境光干扰复杂度会成倍增加。另一个关键点是CircuitPython的原生支持。CPX是Adafruit推动CircuitPython生态的旗舰产品之一其固件和库支持非常完善。这意味着我们可以用高级的Python语法直接操作硬件例如analogio库读取模拟量neopixel库控制LED颜色无需接触底层寄存器或复杂的C语言编译环境让开发者能更专注于算法和逻辑本身。注意市面上有一些更便宜的开发板也可能带有LED和光敏电阻但如果它们没有像CPX这样将两者在物理上精密对齐并且没有对光学干扰做板级优化比如在传感器周围做遮光处理那么获取稳定脉搏波形的难度会大大增加。CPX的“开箱即用”体验是其核心价值。2.2 PPG原理探微绿光、血液与微变信号光电容积脉搏波PPG技术的核心在于光与血液组织相互作用的物理学和生理学。为什么常用绿光这主要基于血红蛋白的光吸收特性。含氧血红蛋白HbO2和脱氧血红蛋白Hb在绿光波段约530nm都有一个较强的吸收峰。当绿光照射到皮肤时大部分光被表皮和组织散射吸收但有一部分会穿透到皮下毛细血管床。血液中的血红蛋白会强烈吸收绿光而皮肤、骨骼等其他组织的吸收相对较弱且稳定。随着心脏收缩动脉血涌入毛细血管该区域的血液容积增加吸收的绿光量也随之增加反射回传感器的光强就减弱心脏舒张时则相反。因此反射光强度的周期性微弱调制就编码了脉搏信息。信号特征与挑战我们试图从传感器读取的原始信号是一个极其微弱的交流AC信号叠加在一个巨大的直流DC偏置之上。DC分量来源于皮肤、骨骼、静脉血等对光的静态吸收和反射其强度可能比代表脉搏的AC分量高出数十甚至上百倍。我们的首要任务就是在电路中通过硬件或软件“剔除”这个DC偏置将微弱的AC脉搏信号放大到可以观察和分析的范围。此外这个信号还极易受到干扰手指的轻微移动运动伪影、环境光的变化、呼吸引起的血压波动都会在信号中产生噪声其幅度可能远超脉搏信号本身。因此后续的数据处理中滤波和运动伪影消除是关键。3. 开发环境搭建与基础代码剖析3.1 软件环境准备Mu编辑器的妙用工欲善其事必先利其器。对于CircuitPython开发我强烈推荐使用Mu Editor。它不仅仅是一个代码编辑器更是一个集成了串行终端、绘图器和交互式REPL交互式解释环境的轻量级IDE特别适合教育和快速原型开发。安装与配置流程安装Mu从官网codewith.mu下载对应操作系统的安装包。安装过程简单直观。连接CPX用USB线将CPX连接到电脑。首次连接时电脑会将其识别为一个名为CIRCUITPY的U盘。检查CircuitPython固件打开CIRCUITPY盘符查看根目录下是否有code.py或main.py文件以及lib文件夹。如果没有或板子表现异常可能需要先到Adafruit官网下载最新的CircuitPython UF2固件文件按住CPX上的复位键或通过双击复位按钮进入引导加载模式将UF2文件拖入出现的CPLAYBOOT盘符进行刷写。启动Mu并选择模式打开Mu在启动界面或模式菜单中选择“CircuitPython”。此时编辑器底部应出现一个串行终端窗口显示CircuitPython的版本信息和提示符。Mu绘图器Plotter是本项目的“眼睛”。它可以将通过print()语句输出的数值实时绘制成滚动波形图。这对于调试模拟信号、观察滤波效果、最终确认脉搏波形至关重要。在后续编码中我们会将处理后的数据以特定格式打印从而在绘图器中可视化。3.2 基础代码逐行解读与优化让我们深入分析项目提供的核心代码并理解其每一部分的意图同时我会分享一些优化思路。import time import analogio import board import neopixel def sign(value): if value 0: return 1 if value 0: return -1 return 0导入与辅助函数代码开头导入了必要的库。analogio用于读取模拟输入光传感器neopixel控制LED。sign()是一个简单的符号函数用于判断一个数值是正、负还是零这在后续检测信号过零点即波形从负到正或从正到负的穿越点时非常有用。pixels neopixel.NeoPixel(board.NEOPIXEL, 10, brightness1.0) light analogio.AnalogIn(board.LIGHT) # Turn only pixel #1 green pixels[1] (0, 255, 0)硬件初始化这里创建了NeoPixel对象控制全部10颗LED和模拟输入对象连接光传感器。关键的一步是pixels[1] (0, 255, 0)它将索引为1的NeoPixel根据板子布局通常是位于边缘、靠近光传感器的那一颗设置为纯绿色作为我们的PPG信号发射光源。亮度设为最大值255。# How many light readings per sample NUM_OVERSAMPLE 10 # How many samples we take to calculate average NUM_SAMPLES 20 samples [0] * NUM_SAMPLES采样参数定义这里引入了两个重要的概念。NUM_OVERSAMPLE过采样次数10由于模拟读数可能存在随机噪声快速连续读取10次然后取平均可以有效平滑掉高频噪声提高单次采样值的信噪比。这是一种简单而有效的软件滤波。NUM_SAMPLES样本窗口大小20我们维护一个长度为20的数组samples用于存储最近20个经过过采样平均后的光强值。这个窗口将用于计算实时移动平均以消除信号的直流DC偏置。lasttime time.monotonic() while True: for i in range(NUM_SAMPLES): # Take NUM_OVERSAMPLE number of readings really fast oversample 0 for s in range(NUM_OVERSAMPLE): oversample float(light.value) # and save the average from the oversamples samples[i] oversample / NUM_OVERSAMPLE # Find the average主循环与数据采集程序进入一个无限循环。内层循环遍历样本窗口的每个位置i从0到19。在每个位置上它执行一个快速的过采样循环累加10次原始光强读数light.value然后计算平均值存入samples[i]。light.value的范围通常是0-6553516位ADC代表光强。mean sum(samples) / float(len(samples)) # take the average print((samples[i] - mean,)) # center the reading实时去直流DC Removal与绘图这是算法的核心之一。在每次获得一个新样本后立即计算当前整个20个样本窗口samples的算术平均值mean。这个mean值近似代表了当前时刻信号的直流分量。然后将最新样本samples[i]减去这个均值得到(samples[i] - mean)。这个操作移除了直流偏置将信号“居中”到零线附近凸显出交流的脉搏波动。结果通过print((value,))注意括号和逗号构成一个元组输出到串口Mu绘图器就能捕捉并绘制这个值。if i 0: # If the sign of the data has changed munus to plus # we have one full waveform (2 zero crossings), pulse LED if sign(samples[i]-mean) 0 and sign(samples[i-1]-mean) 0: pixels[9] (200, 0, 0) # Pulse LED else: pixels[9] (0, 0, 0) # Turn LED off time.sleep(0.025) # change to go faster/slower心跳检测与视觉反馈如果当前索引i0即至少有两个样本可用于比较代码会比较当前去直流后的信号sign(samples[i]-mean)和前一个信号sign(samples[i-1]-mean)的符号。条件sign(当前) 0 and sign(前一个) 0检测的是信号从正数变为零或负数的时刻吗不仔细看它检测的是从正到非正零或负的过零点。但注释写的是“minus to plus”负到正。这里存在一个潜在的逻辑歧义或注释错误。通常检测一个完整周期如心跳会寻找特定的特征点比如从负到正的过零点上升沿零点或峰值。原代码的条件更可能是在检测信号的下降沿过零点。为了更清晰地指示心跳我们可以修改为检测从负到正的过零点这通常对应脉搏波的上升支起点更直观if sign(samples[i]-mean) 0 and sign(samples[i-1]-mean) 0: # 检测负到正的过零点 pixels[9] (200, 0, 0) # Pulse LED else: pixels[9] (0, 0, 0) # Turn LED off当检测到过零点时点亮索引为9的NeoPixel通常位于另一侧为红色否则熄灭它从而形成一个随心跳闪烁的视觉指示。最后的time.sleep(0.025)控制每次采样间隔约为25毫秒即采样率约为40Hz。对于心率通常3Hz来说这足够了。4. 实操步骤、信号优化与心率计算4.1 手指摆放技巧与信号调校将代码上传只需保存为CIRCUITPY盘根目录下的code.py板子会自动运行后真正的挑战才开始获取一个清晰的波形。正确覆盖找到板子上发绿光的LED#1和旁边的“眼睛”图标光传感器。用指腹最好是食指或中指轻轻但完全地覆盖这两个区域。手指应与板子表面垂直或稍微倾斜确保接触稳定没有环境光从边缘漏入。手指方向建议与USB电缆平行这符合人体工学更容易保持稳定。观察绘图器打开Mu的绘图器Plotter。初始时你可能会看到一条剧烈跳动的线或一个缓慢漂移的信号。这是因为程序正在用初始的20个样本建立平均值且你的手指可能还未稳定。压力调校这是最需要耐心和技巧的一步。压力过大会压迫毛细血管反而减弱了血液容积变化导致信号幅度太小绘图器上的波形波动范围可能只有正负几十或几百。压力过轻则接触不稳环境光干扰和运动伪影会占主导波形杂乱无章或幅度超大远超±1000。目标调整手指按压力度目标是让绘图器上居中后的波形samples[i]-mean在一个相对稳定的基线上下波动且波峰-波谷的幅度峰峰值大约在1500到2500之间原教程说±1000即峰峰值2000是合理的参考。波形应呈现出类似正弦波或三角波的周期性。技巧尝试用另一只手轻轻托住拿着板子的手肘或将板子放在桌面上手指轻轻搭上去利用手臂或桌面的支撑来减少肌肉震颤。保持均匀呼吸暂时屏息有助于观察更干净的信号。4.2 算法优化提升检测稳定性原始代码的检测逻辑相对简单在实际环境中可能因噪声而产生误触发。我们可以引入一些优化幅度阈值过滤过零点检测对噪声非常敏感。一个微小的噪声毛刺穿过零线就可能误触发一次“心跳”。我们可以增加一个幅度条件只有当信号值在过零点附近同时满足一定的绝对值条件时才认为是有效心跳。例如可以要求前一个样本负值低于某个负阈值当前样本正值高于某个正阈值。current_val samples[i] - mean prev_val samples[i-1] - mean NEG_THRESHOLD -50 # 负向阈值需根据实际信号调整 POS_THRESHOLD 50 # 正向阈值 if sign(current_val) 0 and sign(prev_val) 0: if prev_val NEG_THRESHOLD and current_val POS_THRESHOLD: # 有效的、幅度足够的心跳过零点 pixels[9] (200, 0, 0)简单移动平均滤波在对整个窗口求均值去直流之前可以先对原始采样值或过采样后的值进行一轮移动平均滤波进一步平滑高频噪声。例如维护一个长度为5的移动平均窗口。动态调整采样率与窗口原代码固定采样间隔25ms40Hz。对于静坐测量可以尝试稍微降低采样率如time.sleep(0.04) 25Hz有时能获得更平滑的波形。NUM_SAMPLES窗口大小20决定了用于计算均值的“历史长度”。窗口太短均值容易受近期波动影响不稳定窗口太长对信号缓慢漂移的跟踪能力变差。可以尝试调整为15或25进行对比。4.3 从波形到心率计算方法与精度探讨当红色LED#9开始随着你的心跳规律闪烁时就可以计算心率了。手动计时法15秒法用秒表计时15秒同时计数红色LED闪烁的次数。将次数乘以4得到每分钟心跳数BPM。例如15秒内闪烁18次心率约为72 BPM。60秒法直接计时一整分钟计数闪烁次数。这是最准确的手动方法但需要更长时间保持手指稳定。代码自动计算法进阶 我们可以修改代码使其自动计算并打印心率。思路是记录每次检测到有效心跳的时间戳计算连续心跳的时间间隔RR间期然后转换为BPM。import time # ... 其他导入和初始化 ... last_beat_time 0 beat_count 0 bpm 0 while True: # ... 数据采集、去直流、过零点检测逻辑 ... if 检测到有效心跳: current_time time.monotonic() if last_beat_time 0: # 计算心跳间隔秒 beat_interval current_time - last_beat_time # 防止除零和异常间隔例如间隔在0.3到2.0秒之间对应心率30-200BPM if 0.3 beat_interval 2.0: # 计算瞬时心率 instant_bpm 60.0 / beat_interval # 可以使用滑动平均来平滑瞬时心率值 bpm 0.8 * bpm 0.2 * instant_bpm # 一阶低通滤波 print(BPM: {:.1f}.format(bpm)) last_beat_time current_time beat_count 1 pixels[9] (200, 0, 0) else: pixels[9] (0, 0, 0) time.sleep(0.025)精度管理预期正如教程所述这种简易装置的精度有限误差可能在±10%甚至更大。影响精度的因素包括手指压力与位置的微小变化、皮肤色素/厚度/温度的个体差异、运动伪影、环境光干扰、以及算法对波形特征点检测的准确性。它适用于心率趋势观察、静息心率粗略测量或教育演示但不能用于医疗诊断。5. 常见问题、深度排查与项目扩展5.1 问题排查速查表问题现象可能原因排查与解决步骤绘图器没有波形是一条直线1. 手指未正确覆盖传感器和LED。2. 环境光太强饱和了传感器。3. 代码未运行或打印格式错误。1. 确认绿色LED#1亮起。在暗处操作确保手指紧密、完全覆盖。2. 检查Mu是否连接到正确的串口绘图器窗口是否打开。确认代码中print((samples[i]-mean,))的括号和逗号格式正确。波形幅度太小 ±5001. 手指按压过重血流受限。2. 绿色LED亮度不足或损坏。3. 采样参数不合适。1.减轻压力让指尖自然贴合即可。2. 检查pixels[1] (0, 255, 0)是否执行。尝试调高亮度但注意别太刺眼。3. 尝试减小NUM_OVERSAMPLE或NUM_SAMPLES让系统响应更快。波形杂乱、毛刺多、幅度超大 ±20001. 手指按压太轻或接触不稳运动伪影。2. 环境光干扰。3. 身体在移动或呼吸过深。1.稍微增加压力确保稳定接触。将手肘支撑在桌面。2. 在较暗环境中测试。3. 保持静止短暂屏息观察波形是否变干净。红色LED闪烁不规则或完全不闪1. 信号波形不规整过零点检测算法失效。2. 检测阈值设置不当。3. 心率超出算法预期范围太快或太慢。1. 首要任务是优化手指位置和压力获得干净、周期性的波形。2. 在代码中增加打印原始(samples[i]-mean)的值观察其是否规律跨越零线。3. 尝试调整time.sleep值改变采样率。Mu绘图器显示“ValueError”或数据断断续续1. 串口通信不稳定或中断。2. 代码中有语法错误或运行时错误。3. USB供电不足。1. 尝试拔插USB线在Mu中重新连接串口。2. 检查Mu的串行终端是否有错误信息。简化代码测试。3. 使用质量好的USB线并直接连接电脑主板端口避免经过扩展坞。5.2 项目扩展思路掌握了基础版本后这个项目有丰富的扩展可能性多色LED与血氧模拟CPX的NeoPixel可以发出不同颜色的光。研究表明血红蛋白对红光和红外光的吸收特性在含氧和脱氧状态下差异显著。你可以尝试编写程序交替点亮红色和红外色可用深红色近似LED并分别读取传感器值。通过分析两种光信号吸收率的比值可以非常粗略地模拟血氧饱和度SpO2测量的原理。请注意这需要非常精密的校准和算法结果仅供概念演示。数据记录与分析将实时的心率数据BPM或原始波形数据写入到CPX的存储空间open(“/hr_log.txt”, “a”)作为一个简单的数据记录器。之后可以将文件复制到电脑用Python的Pandas、Matplotlib进行更深入的分析比如计算平均心率、心率变异性HRV的简单指标等。无线传输与可视化为CPX搭配一个蓝牙或Wi-Fi模块如Adafruit的ESP32协处理器板将心率数据实时发送到手机App或网页服务器实现远程监控和更华丽的可视化。结合其他传感器利用CPX内置的加速度计可以尝试进行简单的运动状态识别静坐、走路、跑步。在运动时PPG信号会受到严重干扰此时可以暂停心率显示或切换到基于加速度信号估算心率的模式虽然精度更低。优化信号处理算法在代码中实现更专业的数字信号处理算法如带通滤波器只保留0.5Hz到3Hz左右的心率信号或使用峰值检测算法代替简单的过零点检测以提高心率计算的抗干扰能力和准确性。这个项目就像一扇门推开它你看到的是嵌入式系统、生物医学传感和信号处理这个广阔领域的缩影。从指尖那一缕微弱的绿光开始通过代码将其转化为有节奏的闪烁和跳动的波形这个过程本身就是硬件与软件、物理与生理、感知与计算最美妙的结合。