拓冰建站拓冰建站
首页 / 资讯中心 / 正文

AFE4400血氧检测开发实战:从寄存器配置到SpO2算法实现

简介面向血氧监测与可穿戴健康设备开发者此压缩包提供 TI AFE4400 模拟前端芯片的驱动源码解决从光电信号采集到血氧饱和度计算的关键实现问题。AFE4400 集成光源驱动器、光电二极管接收器、信号调理电路与 ADC在医用血氧仪、健康监护和运动手环中广泛应用。包内共 2 个文件、仅 4KB1 个 C 源文件负责芯片初始化、寄存器配置、数据采集与信号处理并包含从原始光电信号提取脉搏波的分析思路1 个头文件定义函数原型、常量与数据结构方便模块化调用。目前已有 1018 人学习下载适合具备一定嵌入式基础、希望快速掌握 AFE4400 驱动框架的开发者。参考该源码可缩短血氧监测产品原型验证周期为低功耗、高精度的可穿戴健康设备开发提供便捷起点。1. AFE4400是什么TI那颗专为血氧长出来的模拟前端AFE4400血氧程序这个标题拆开看是三个词AFE4400、血氧、程序。AFE4400是TI推出的手机/医疗级模拟前端芯片专门为血氧饱和度SpO2和心率检测设计。它内部把LED驱动、跨阻放大器TIA、环境光消除回路、采样保持器和ADC全部集成在一起MCU只通过I2C写寄存器、读数据不需要自己搭运放和模拟开关这是它能替代分离方案的核心原因。不过压缩包里的例程解决的是能跑的问题不解决跑得准的问题。血氧值的准确性依赖寄存器配置是否合理、采样频率和探头参数是否匹配、以及算法对AC/DC分量的分离是否干净。这篇文章按寄存器怎么配 → 数据怎么读 → 信号怎么算 → 现场怎么调这条链路展开适合正在调AFE4400、AFE4403这类afe44*系列芯片的工程师。你能看到的不只是初始化代码还有TI FAE到现场后先看哪些寄存器的判断习惯。2. AFE4400寄存器配置从CONTROL0到LED电流的初始化链路2.1 必须认识的8个关键寄存器AFE4400的寄存器数量不多但每个都有明确分工。我把上电后必须配置和关注的寄存器列在这张表里后面的代码和调试过程都以这张表为基准。值得留意的是AFE4400与AFE4403、AFE4404有几个寄存器地址不兼容比如AFE4404就精简了部分时序控制位所以例程不能直接互通需要先对一下数据手册的寄存器映射。寄存器名地址作用初始化关注点CONTROL00x00芯片工作模式与启动控制bit0写1才能配置其他寄存器CONTROL10x01单脉冲/多脉冲模式、LED脉宽脉宽影响信号强度和功耗TIA_GAIN0x0C跨阻放大增益太小信号弱太大量程饱和LEDCNTRL0x0D红光与红外LED电流每档电流约对应固定毫安数PRPCOUNT0x0E采样脉冲周期直接决定采样频率LED1VAL0x21红光PPG累积值只读计算R值的分子LED2VAL0x20红外PPG累积值只读计算R值的分母ALED2VAL0x22环境光采样值调试时判断环境光干扰表中ALED2VAL是最容易被忽略的寄存器。正常使用时算法里用不到它但它记录的是LED不点亮时环境光对光电二极管的贡献。如果这个值超过LED1VAL直流分量的十分之一说明光学结构漏光测出来的血氧在强光环境下会飘。所以我的调试串口里永远会把这第三个值一起打出来。2.2 寄存器初始化顺序比想象中敏感AFE4400有一个特性很多寄存器在工作状态下写入不会立即生效或者生效瞬间会让输出波形出现毛刺。TI例程普遍遵循一个固定顺序先通过CONTROL0把芯片停下写PRPCOUNT、TIA_GAIN、LEDCNTRL最后再切回工作状态。我第一次调试时图省事没停芯片直接改了TIA_GAIN结果I2C能读到数据但波形上每隔一段就出现一个尖刺。后来按顺序改尖刺消失。原因是TIA增益切换时后级滤波电容上的电荷还没稳定采样保持电路会把跳变过程采集进去。FAE上门排查这类问题第一步就是看你初始化代码里有没有这个停-配-启的动作。void AFE4400_Init(void) { // 1. 进入配置模式停止内部定时器 AFE4400_WriteReg(0x00, 0x01); // 2. 采样周期PRPCOUNT 7999 // 4MHz时钟下采样率 f_s 4M / (7999 1) ≈ 500Hz AFE4400_WriteReg(0x0E, 0x1F3F); // 3. TIA增益与量程配置典型值让直流分量落在20%~60% AFE4400_WriteReg(0x0C, 0x0010); // 4. LED电流红光和红外先给一个保守值 AFE4400_WriteReg(0x0D, 0x0020); // 5. CONTROL1单脉冲模式脉宽取默认值 AFE4400_WriteReg(0x01, 0x0111); // 6. 切回工作状态启动定时器 AFE4400_WriteReg(0x00, 0x00); }这段代码里的寄存器值都是典型初始配置不是所有板子的通用解。PRPCOUNT的0x1F3F换成十进制是79994MHz晶振下采样率正好接近500Hz。为什么血氧要500Hz而不是像体温那样几赫兹就够因为AFE4400内部每个采样周期会对LED点亮期间的ADC结果做累积采样率越高单次累积次数越少但单位时间内数据点更多后面的软件滤波有更大处理空间。TIA_GAIN的0x0010对应的是中间档位信号饱和就降档信号太弱就升档。2.3 动态范围和TIA增益怎么对判断TIA增益配得对不对标准只有一个稳定贴上探头后LED1VAL和LED2VAL的直流分量占满量程的20%到60%。AFE4400的ADC是22位满量程约0x3FFFFF也就是说直流分量应该落在80万到250万这个范围。低于20%说明信号太小要么LED电流不够要么TIA增益太低高于60%说明静态工作点偏高留给脉动分量的余量不足稍微动一下就会削顶。具体操作顺序是先固定TIA_GAIN用LEDCNTRL把直流分量调到量程中间偏下一点然后再回头观察PPG波形的交流幅度如果太小再升一档TIA_GAIN而不是两个寄存器一起动。同时调两个参数你就永远判断不了是哪个起了作用。这个先直流后交流的顺序就是TI DC诊断流程里最核心的执行逻辑后面第5章还会用到。3. 用MCU把AFE4400跑起来I2C时序与数据读取3.1 I2C地址和读数据前的寄存器指针AFE4400的7位I2C地址是0x58换算成8位就是写0xB0、读0xB1。这个地址是芯片出厂固定的不像某些传感器支持地址引脚修改所以多片AFE4400挂同一条I2C总线时必须用I2C开关或者分时供电来解决争用。读数据有个坑不能直接发读命令。AFE4400内部有一个地址指针它指向哪个寄存器读命令就返回哪个寄存器的值。上电后指针默认指向0x00如果你直接读拿回来的是CONTROL0的状态。正确做法是先发一个写命令把目标寄存器地址写进指针然后重发Start信号切换为读操作。这个Restart时序在逻辑分析仪上看到的就是两次Start中间夹一个寄存器地址字节。uint16_t AFE4400_ReadReg(uint8_t regAddr) { uint8_t buf[2]; uint16_t value; I2C_Start(); I2C_SendByte(0x58 1); // 写地址 I2C_SendByte(regAddr); // 设置内部寄存器指针 I2C_Start(); // 重启I2C总线 I2C_SendByte((0x58 1) | 1); // 读地址 buf[0] I2C_RecvByte(); I2C_Ack(0); // 倒数第二字节回ACK buf[1] I2C_RecvByte(); I2C_Stop(); value (buf[0] 8) | buf[1]; return value; }这段代码的关键是第二个I2C_Start。在标准I2C读流程里指针设置和读数据必须由Start隔开如果中间插入其他设备的数据传输指针就会被覆盖。还有一处细节I2C_RecvByte后回ACK还是NACK取决于后面还有没有字节要收。上面代码里第一个字节回ACK第二个字节之后回NACK这是标准I2C读多字节的收尾方式。很多从例程抄来的代码只写了两个连续读没注意最后一个字节必须回NACK会导致主机认为从设备还要继续发数据总线状态卡死。3.2 连续采集的代码实现实际工程里不会一个寄存器一个寄存器地读而是把LED1VAL、LED2VAL、ALED2VAL当成一组连续数据读取。AFE4400支持burst读可以从0x20开始连续读6个字节正好覆盖这三个寄存器。下面这段代码放在定时器中断里执行采样率500Hz读取频率也是500Hz。#define AFE4400_BASE_ADDR 0x58 void AFE4400_ReadPPG(uint32_t *red, uint32_t *ir, uint32_t *amb) { uint8_t buf[6]; I2C_Start(); I2C_SendByte(AFE4400_BASE_ADDR 1); I2C_SendByte(0x20); // 从LED2VAL开始连续读 I2C_Start(); I2C_SendByte((AFE4400_BASE_ADDR 1) | 1); for (int i 0; i 5; i) { buf[i] I2C_RecvByte(); I2C_Ack(1); // 前5个字节回ACK } buf[5] I2C_RecvByte(); I2C_Ack(0); // 最后一个回NACK I2C_Stop(); *ir (buf[0] 16) | (buf[1] 8) | buf[2]; *red (buf[3] 16) | (buf[4] 8) | buf[5]; // amb的值需要单独再读0x22或者把三次读简化成三次ReadReg }注意这里的寄存器顺序和命名容易搞混0x20对应LED2VAL0x21对应LED1VAL。因为AFE4400数据手册里把红外定义为LED2红光定义为LED1。所以上面代码里读回来的buf[3..5]才是红光赋值给*red。我在代码里把两个变量名和地址的对应关系写进注释就是怕后面维护的人把红和红外的顺序换掉。这种通道顺序错误在成品里出现过症状是血氧算出来恒定在100%附近因为R值接近1时红和红外对调后公式仍能算出一个看似合理的值。I2C上拉电阻也值得检查。AFE4400的I2C接口不是开漏标准板子通常已经带了上拉但如果NACK一直出现先翻原理图确认上拉电阻接的是1.8V还是3.3V电源域AFE4400的数字IO电压由VDD2决定上拉电源必须和它一致不一致时电平标准不匹配高速读连续字节时容易出错。3.3 采样率与PRPCOUNT的坑采样率计算公式是 f_s f_CLK / (PRPCOUNT 1)其中f_CLK是AFE4400内部时钟默认来自外部晶振常见配置是4MHz。如果板子上放的是8MHz晶振代码里沿用4MHz时的PRPCOUNT7999实际采样率会翻倍到正常值以上后面滤波器参数全偏。这个问题的最快验证方法不是读寄存器而是拿示波器勾芯片的LED2引脚。LED2引脚在脉冲周期内会输出方波脉冲数一下1秒内有多少个脉冲就是实际采样率。我在调试时习惯先做这个动作确认采样率对了再去调算法。还有一种情况PRPCOUNT设了但没生效读回0x0E寄存器确认写入值是否还在有时I2C写时序出错寄存器根本没写进去但MCU这边不报错数据读回来也是旧的。采样率不是越高越好。血氧信号主能量集中在0.1Hz到5Hz500Hz采样远超奈奎斯特要求这是为了给LED累积采样提供余量。如果为了省电把PRPCOUNT拉大到采样率降到100Hz波形会明显变毛糙。功耗和信噪比的平衡点实际项目中我通常从500Hz往下试探每降一档观察一次PPG波形。降到200Hz以下时AC幅值抖动会明显增加那时就停手。4. 血氧算出来从PPG原始值到SpO2的完整处理链4.1 为什么不能直接拿两个ADC值做比值很多人拿到AFE4400压缩包跑通串口打印后第一个想法是SpO2是不是等于红外除以红光方向对但直接用原始值算会得到完全没用的结果。血氧算法的理论依据是朗伯-比尔定律的变体关键在透过组织的光吸收分为两部分一部分是动脉血脉动引起的交流分量AC另一部分是组织、静脉血和固定吸收产生的直流分量DC。血氧饱和度由这两路光的AC/DC比值决定R (AC_red / DC_red) / (AC_ir / DC_ir)直接拿LED2VAL除以LED1VAL相当于把AC和DC混在一起手指稍微动一下结果就全变。正确做法是先把PPG波形分解出AC和DC再代入公式。这也是为什么很多血氧例程看起来代码量不大但真正算得稳的需要在算法前面加一大段滤波处理。4.2 滤波和基线漂移处理PPG原始波形的处理链我通常按三步走第一步低通滤波去掉高频噪声第二步高通或带通滤掉基线漂移第三步做平滑。基线漂移主要来自呼吸运动频率在0.1Hz到0.5Hz之间脉搏波频率在0.5Hz到4Hz之间。所以带通的下限取0.5Hz、上限取5Hz能把这两类干扰都压下去。下面给一段在PC上验证滤波器参数的Python代码。算法拿到MCU上移植前先用这段代码在电脑上跑通确认参数合适再动手写C版本能省掉大量烧录-观察-再烧录的时间。import numpy as np from scipy import signal def ppg_preprocess(red, ir, fs500.0): # 带通滤波0.5Hz~5Hz分离出交流分量 b, a signal.butter(2, [0.5/(fs/2), 5.0/(fs/2)], btypebandpass) red_ac signal.filtfilt(b, a, red) ir_ac signal.filtfilt(b, a, ir) # 低通滤波截止频率0.5Hz提取直流分量 b2, a2 signal.butter(2, 0.5/(fs/2), btypelowpass) red_dc signal.filtfilt(b2, a2, red) ir_dc signal.filtfilt(b2, a2, ir) return red_ac, ir_ac, red_dc, ir_dcfiltfilt是零相位滤波和MCU里常用的IIR滤波比它不会产生相位偏移在PC上验证算法时能真实反映幅值关系。但filtfilt有边界效应开头和结尾各有一段数据不能用所以实际计算R值时我会把滤波后的前100个点扔掉再算。MCU端如果存储器不够存大批数据可以改用级联二阶IIR滤波器相位延迟靠后续算法统一补偿。滤波阶数选择有讲究。二阶巴特沃斯带通的滚降不够陡如果环境光里有强烈的工频干扰会在波形上残留明显纹波升到四阶带通又带来更大的群延迟。我的做法是保留二阶滤波同时在MCU里加一个50Hz陷波专门抑制工频噪声。这里需要注意陷波滤波器的Q值不能用默认值血氧信号本身能量集中在0.5到5Hz陷波带宽取2Hz足够Q值设太高反而会在陷波频率附近引入振铃。波峰波谷的识别也依赖这个滤波窗口。一个窗口取多少数据取决于脉率。正常成人脉率60到100次/分对应周期0.6秒到1秒500Hz采样下就是300到500个点。我一般取1024点的窗口做快速傅里叶变换这样频率分辨率在0.5Hz能清楚分辨0.5Hz和0.9Hz两个相邻频率峰值。FFT的幅度谱里红光和红外两路的主导频率应该一致如果两路主峰频率对不上说明有一路信号质量太差这时候直接放弃这个窗口的数据不参与R值计算这是提高血氧稳定性的一个关键细节。4.3 R值到SpO2的拟合公式算出血氧饱和度的最后一步是把R值映射到SpO2。因为不同波长LED的吸光系数差异这个映射需要通过实验标定。早期设备普遍用一条线性公式SpO2 110 - 25 × R这个公式是可用的起点但不同板子上LED波长存在离散性系数会漂一点。R值落在0.4到1.0之间时线性拟合误差大约在正负2%以内。做消费级血氧产品这精度够了做医疗级就必须采购血氧模拟器做多点标定。下表是线性公式展开后的对应关系注意这只是教学参考不是通用标定表。R值SpO2线性拟合值0.4100%0.597%0.695%0.792%0.890%0.987%1.085%在实际项目里我用血氧模拟器给某批探头标定后得到一组系数SpO2 104.5 - 21.6 × R。这组系数只适用于那批LED换一批LED就要重新标定。所以压缩包里的固定系数可以让你先看到波形但真实产品的系数必须自己做。标定方法是用模拟器输出预设SpO2值采集对应的R值做最小二乘拟合至少取5个点覆盖70%到100%取的点越多线性拟合越稳。MCU端计算时有个容易被忽视的坑整型溢出。AC和DC都是32位值做除法时如果直接用整数相除得到的结果是整数而不是浮点数R值会严重跳变。正确做法是先转成float再除。代码上这样写float compute_spo2(uint32_t red_ac, uint32_t red_dc, uint32_t ir_ac, uint32_t ir_dc) { float r; // 异常保护交流分量太小说明探头未贴合 if (ir_ac 10 || red_ac 10) { return -1.0f; // 返回无效值由上层沿用上一次结果 } r ((float)red_ac / red_dc) / ((float)ir_ac / ir_dc); return 110.0f - 25.0f * r; }这段代码里的-1.0f是约定好的无效值。算法层拿到-1时直接丢弃不更新显示。这个保护非常重要因为手指刚贴上探头时信号还没稳定AC幅值极低如果不加保护SpO2会从70%到100%乱蹦一阵。很多血氧模块启动时数值剧烈跳变原因就在这里。5. AFE4400调试落地TI DC诊断流程和三个稳定技巧5.1 把TI的DC诊断流程变成你的排查顺序TI FAE在支持血氧类问题时的第一动作不是看算法而是打开串口打印LED1VAL、LED2VAL、ALED2VAL三个值先确认DC是否正常。这套流程叫DC诊断核心就一条DC决定光学通路AC决定信号质量。先把DC问题排除掉再谈滤波和算法。我按这个顺序排查基本能命中九成问题。观察现象可能原因处理动作两路DC都很小LED电流太小或探头未贴合增大LEDCNTRL检查贴合红DC正常、红外DC极小红外LED虚焊或损坏测LED引脚压降确认两路DC都接近满量程环境光干扰严重检查遮光结构降低环境光ALED2VAL超过DC的10%光学结构漏光加遮光泡棉重测上述现象里ALED2VAL是一个容易漏掉的定位点。它直接代表环境光贡献不需要拿光照计就能判断板子是否漏光。我遇到过一个案例机器在室内测血氧稳定拿到户外阳光下数值直接飘到92%量ALED2VAL发现它占总DC的30%。换了遮光泡棉后户外数据恢复正常。5.2 验证算法链路的离线复现法判断板上算法到底算得对不对我建议先把一段原始数据导到电脑上离线复算。做法是固定探头不动连续采集10秒红光和红外数据用串口导出CSV。在Python里按第4章的滤波流程处理后手工找波峰波谷算AC和DC得到R值。把这套手工计算的SpO2和板上输出的SpO2对比误差在2%以内说明算法链路没有问题误差大先回头查滤波器的截止频率和相位延迟而不是怀疑拟合公式。这个离线复算还有一个额外用途确认采样率标识是否一致。串口导出的数据点如果每两个相邻点的时间间隔是2ms说明500Hz确实生效如果间隔超过2ms说明MCU读取有丢点I2C速度不够时需要开启硬件I2C或者降低采样率。AFE4400读取的数据量并不大500Hz下双通道加环境光也就6KB/s给系统里其他功能留下的总线余量充足这段传输数据如果准备走射频链路上传比如对接CC1120这类窄带收发器完全足够。5.3 三个越用越稳的落地技巧最后说三个具体的稳定性技巧。第一个是每次改动寄存器时记录旧值和新值。AFE4400的寄存器之间相互影响比如改了TIA增益后直流分量会变这时往往还需要同步调整LEDCNTRL。没有记录第二天就忘了上次稳定状态是哪组参数。我的做法是在配置代码里留一份默认值宏调试时改寄存器值同时把改动记在代码注释里稳定后再覆盖掉默认宏这样每次改动都有据可查。第二个技巧是给SpO2输出加迟滞。人体血氧变化本身是缓慢过程如果计算结果在94%和95%之间反复跳变人眼看起来就是屏显一直在闪。做法是在显示层加一个迟滞窗口新值比旧值大1%以上才更新或者新值比旧值小1%以上才更新中间范围沿用旧值。这个迟滞窗口对运动伪差干扰也有明显抑制作用不需要改算法就能让显示稳定下来。第三个技巧是开机后丢弃前2秒的数据。AFE4400上电瞬间LED电流还没稳定滤波器和环境光消除回路都在建立过程中这2秒算出来的SpO2基本都是无效值。例程里如果直接进主循环算血氧就会看到数值从70多慢慢爬到接近真实的95。在应用层加一个2秒的丢弃窗口显示上直接给测量中比任何滤波算法都更省事也让用户观感更专业。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门