简介本资源是一套基于STM32F103C8T6的健康与运动监测手环完整设计工程面向嵌入式初学者、课程设计学生及毕业设计开发者解决心率、血氧、血压、体温多参数实时采集与阈值预警的典型物联网终端开发问题。压缩包含278个文件涵盖56个C源文件如stm32f10x_adc.c、stm32f10x_i2c.c等外设驱动、57个编译中间文件.d、38个头文件.h及Proteus8.15仿真工程.pdsprj、Keil工程.uvprojx、HEX固件与PDF原理说明等总大小14.1MB结构完整、模块清晰便于理解传感器驱动、OLED人机交互与多任务阈值管理逻辑。已有248人学习下载配套功能详实支持OLED主页实时显示四类生理参数通过独立按键进入五级阈值设置心率上限/血氧下限/舒张压/收缩压异常时蜂鸣器报警特别优化了JFC103模块动态启用逻辑与DS18B20常驻测温机制兼具实用性与教学示范性。 这阵子自己动手从零做了一款基于STM32的健康与运动监测手环从画原理图到写驱动再到算法调试完整走了一遍。这篇东西不打算写成教程式说明书主要是想聊聊我在这个项目里为什么这么选型、代码怎么组织、遇到哪些坑是网上教程不会告诉你的。如果你正准备做类似的可穿戴设备或者刚接触STM32想找一个功能相对完整、能直接上手的练手项目这篇内容应该能帮你省下不少时间。先交代一下背景。这个手环的定位不是应付毕设的那种能亮灯就行而是奔着日常佩戴的真实需求去的心率、血氧、计步、睡眠监测、抬手亮屏、消息提醒、续航至少两天。主控选了STM32F103系列传感器选了MAX30102和MPU6050屏幕用0.96寸OLED电池是一块400mAh的锂聚合物电池。后面整个方案的设计和踩坑记录都是围绕这套核心配置展开的。1. 整体方案设计与硬件选型1.1 功能需求拆解与系统架构做可穿戴设备最容易犯的错就是一上来堆硬件。我在项目启动前先列了一份需求清单把功能拆成核心功能和附加功能两层。核心功能是健康监测心率、血氧和运动监测计步、卡路里附加功能包括睡眠监测、抬手亮屏、来电提醒。这样拆的好处是当你遇到性能瓶颈或开发周期紧张时可以暂时砍掉附加功能主架构不受影响。系统架构上我采用单MCU方案没有额外加协处理器。STM32F103C8T6的72MHz主频跑这些任务绰绰有余即使是FreeRTOS加几个传感器任务也毫无压力。传感器全部通过I2C总线挂载OLED通过SPI驱动比I2C快很多刷屏不闪烁电池电压通过ADC采集另外预留了一路串口用于调试和固件升级。系统整体框图大致是STM32F103C8T6作为主控内部通过I2C1连接MAX30102和MPU6050通过SPI1连接OLED显示屏ADC1采集电池电压一个按键负责菜单切换和交互马达用于振动提醒电源部分由TP4056充电芯片加锂电池保护电路组成。1.2 MCU与传感器选型逻辑主控选择上我最终敲定STM32F103C8T6而不是更新潮的G0系列或者L4系列。原因很现实资料多、踩坑经验丰富随便搜一个问题都能找到解决方案这一点在做实物项目时非常重要。虽然L4的低功耗更出色但F103C8T6在适当优化后也能实现两天续航对入门级项目来说完全够用。如果你后续想真正追求续航可以平移代码到STM32L431外设基本兼容迁移成本不高。传感器选型也是反复比较过的。心率血氧用MAX30102这颗芯片集成红光和红外光LED通过PPG原理检测血液容积变化在静息状态下测心率非常准。运动传感器选了MPU6050六轴三轴加速度三轴陀螺仪对于计步、姿态检测来说经典的6050反而比很多新芯片资料更全网上现成的DMP库也能直接移植。这里特别说一句MAX30102模块市面上假货不少我前后买了三块有两块读ID都不对。建议优先选带原理图说明的正规模块到手先读寄存器0xFE和0xFF正确值分别是0x15和0x10读不对就退货别浪费时间在坏模块上。1.3 电源、充电与PCB布局要点手环是锂电供电设备电源设计直接决定产品能不能用。我选了400mAh锂聚合物电池搭配TP4056充电模块。TP4056外围电路非常简单两个电阻设定充电电流我这里用10K电阻对应约130mA充电电流一个LED指示充电状态。电池输出先经过一个3.3V LDO我选了XC6206P332MR再给MCU和传感器供电。PCB布局上有一个容易忽略的点MAX30102尽量放在PCB边缘且朝外一侧不要铺铜、不要走线。因为它是通过绿光/红光反射检测的如果传感器周围有金属走线反射光线会直接干扰信号质量。另外MPU6050要远离I2C总线走线避免高频数字信号耦合到模拟输出上。2. 传感器数据采集与核心代码实现2.1 MAX30102心率血氧驱动与数据滤波MAX30102的驱动整体思路是初始化I2C配置FIFO配置寄存器设置采样率、LED电流然后循环读取FIFO数据。这里有几个经验点。第一LED电流不是越大越好。我试过把红光LED电流调高到满档读出来的PPG波形反而更差背景噪声和运动伪影都会被放大。最终用的红光6.4mA、红外6.4mA静止场景下波形质量最好。第二FIFO采样率设置在400Hz配合平均算法够用又不至于数据量太大。第三读FIFO要用while加超时保护否则一旦I2C总线异常程序会卡死在里面。心率算出来之后必须滤波。原始的PPG信号噪声非常大我用了一套组合拳先做移动平均滤波窗口64点再做一阶差分找波峰波峰间隔就是心跳周期。波峰检测时设置了一个动态阈值threshold data_max - (data_max - data_min) * 0.4这个比例我试了很多值0.4在抗噪声和灵敏度之间平衡得比较好。核心伪代码如下#define PPG_SAMPLE_RATE 400 #define MOVING_WINDOW 64 uint16_t ppg_raw[128], ppg_filtered[128]; void MAX30102_Read_PPG(void) { // 读取FIFO数据 uint16_t red_val, ir_val; MAX30102_Write_Reg(REG_INTR_STATUS_1, 0xC0); // 清除A_FULL和PPG_RDY中断 MAX30102_Read_FIFO(red_val, ir_val); // 移动平均滤波 static uint32_t sum 0; static uint8_t idx 0; sum sum - ppg_raw[idx] red_val; ppg_raw[idx] red_val; ppg_filtered[idx] sum / MOVING_WINDOW; idx (idx 1) % MOVING_WINDOW; }2.2 使用MPU6050时陀螺仪和加速度计的数据融合MPU6050在这套系统里主要负责计步、姿态识别和抬手亮屏。加速度计输出三轴加速度值陀螺仪输出三轴角速度。但在实际佩戴过程中加速度计数据里会混入重力分量直接算步数会非常不准。我使用了MPU6050自带的DMP库直接从DMP输出四元数。这样省去了自己写卡尔曼滤波或互补滤波的麻烦而且DMP在运动状态下表现稳定。DMP库的移植网上资料很多但要注意在初始化时设置MPU6050_RESET后必须延时100ms以上否则DMP自检会失败。读取姿态后抬手亮屏判断逻辑是这样的计算加速度计模值是否大于1.2g同时陀螺仪Z轴角速度绝对值大于60度/秒两者同时满足认为用户正在抬手触发点亮屏幕。这个阈值我实际测试调整过很多次太灵敏会误触发太迟钝又会漏判。2.3 ADC多通道扫描DMA采样不阻塞CPU的配置手环里需要采集电池电压和环境温度使用MCU内部温度传感器这就要用到STM32多通道ADC扫描循环采样配合DMA。我第一次直接用阻塞式HAL_ADC_Start加轮询读值时发现一个严重问题ADC采样期间CPU完全被占用导致传感器读取和显示刷新出现明显卡顿。改用DMA后彻底解决。配置要点有三个ADC工作在扫描模式循环模式规则组通道按顺序排列DMA配置为循环模式数据宽度Half Word内存地址自增启动后不用管CPUDMA会自动把两路ADC数据搬运到缓冲区主循环只需要读取数组里的值就行。uint16_t adc_buf[2]; void ADC_DMA_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 使能时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // ADC1配置扫描模式 循环模式 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 循环模式 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_Init(ADC1, ADC_InitStructure); // 通道顺序通道0 电池电压, 通道1 温度传感器 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // DMA配置 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); }温度传感器内部校准值存在地址0x1FFFF7E8F103读取后结合当前ADC值计算公式是T (V25 - Vdda * ADC_value / 4096) / Avg_Slope 25其中V25约1.43VAvg_Slope约0.0043V/°C。注意这个公式算出来的精度只有±2℃日常看看温度趋势没问题精确测温还是得上外部传感器。2.4 串口空闲中断接收不定长数据的技巧手环和手机App通信是我用串口通过蓝牙模块实现的这里涉及一个老生常谈但很多人搞不定的问题串口接收不定长数据。普通的逐字节接收需要自己判断帧头帧尾如果数据中恰好出现帧头帧尾相同的字节就完蛋了。HAL库的串口空闲中断可以在总线空闲即收到一帧完整数据时触发一次中断配合DMA接收实现零CPU占用接收任意长度的数据包。思路是串口DMA接收通道开启循环模式UART_IT_IDLE中断使能接收到一帧数据后总线空闲时会触发这个中断此时读取DMA当前剩余计数计算本次接收到的字节数然后处理数据包。代码实现上主要涉及两个中断回调的处理。void USART1_IRQHandler(void) { if (RESET ! __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint32_t remaining __HAL_DMA_GET_COUNTER(huart1.hdmatx); // 注意这里是hdmarx uint16_t recv_len RX_BUF_SIZE - remaining; if (recv_len 0) { // 处理收到的数据帧 Handle_Rx_Frame(rx_buffer, recv_len); // 重新启动接收 HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUF_SIZE); } } }这个方案在数据量不大——比如每次几十字节、每秒几次——的场景下非常稳比用帧头帧尾校验省心太多。我自己在联调蓝牙时就是靠这一套跑通的全程没丢过数据。3. 运动算法与健康指标推算3.1 计步算法的基本原理与实现计步是整个手环最核心的运动功能也是很多人做出来之后发现步数完全是乱跳的地方。我从自己的踩坑经验出发把计步算法拆成三部分预处理、波峰检测、步频校验。预处理阶段加速度计原始数据取模值acc_mag sqrt(acc_x^2 acc_y^2 acc_z^2)然后减去1g重力加速度得到动态加速度。这个值在走路时会出现明显的周期性波动静止时接近零。波峰检测阶段用一个简单的状态机当动态加速度从负变正且超过阈值我设为0.3g记一次波峰。但要加一个最短步时间间隔也就是步频上限——正常人最快步频大约每秒3步所以两次波峰之间的间隔必须大于300ms否则视为抖动。步频校验是精确度的关键。设一个滑动窗口缓存最近6个波峰间隔时间如果这些间隔时间的标准差小于20%判定这段序列是正常步行如果标准差过大说明是随机振动直接丢弃。再加上一个简单的时间衰减逻辑静止超过10秒后重置计数器避免原地抖腿也算步的问题。这样一套逻辑在慢走、快走、上下楼场景下实测误差能控制在5%以内。跑步场景下需要把阈值调高到0.5g因为跑步时加速度波动更剧烈。3.2 卡路里与运动距离的推算卡路里消耗不能直接测只能用模型推算。我使用经典METs法即用代谢当量估算每分钟热量消耗热量千卡/分钟 METs × 体重kg × 3.5 / 200。这里的METs值需要根据运动强度动态估算。实际做法是根据加速度模值的均方根值RMS去映射METs。静止状态下RMS约0.05g对应1MET步行约2.5METs快走或慢跑约6METs快速跑约10METs。我做了个简单的分段线性映射实时计算每分钟的METs再乘以体重和时间累加出总消耗。运动距离则是用步数乘以步长。步长不是固定值我根据步频动态估算慢走时步长约是身高的0.35倍快走时约0.45倍。这个公式虽然不如GPS定位准但在没有GPS的手环里已经是主流做法了。3.3 心率变异性HRV与睡眠监测的落地心率变异性是衡量疲劳恢复和压力状态的重要指标但在嵌入式设备上做完整HRV分析不现实因为需要精确到毫秒的R波间期而且对PPG信号质量要求极高。我的实现方式是在静息状态下连续采集60秒PPG信号算出心跳间隔序列然后计算相邻间隔差值的均方根即RMSSD值作为HRV的简化指标。睡眠监测则是基于加速度计和心率的组合判断。加速度计模值持续低并伴有低频周期性波动说明是浅睡心率比清醒时下降5-10次且加速度几乎为零判定为深睡翻身等动作时会有瞬时加速度尖峰作为微觉醒标记。这套逻辑虽然精度不能和医用多导睡眠监测比但作为消费级手环足够了。4. 显示交互、实时系统与低功耗设计4.1 OLED多级菜单状态机设计0.96寸OLED虽然是块小屏幕但菜单逻辑不设计好会非常难维护。我用的是经典的状态机模型每个菜单页面是一个状态按键事件驱动状态跳转。我抽象了一个菜单结构体包含当前页面的绘制函数、按键按下时的处理函数、进入页面的初始化函数。状态跳转的核心是一个事件驱动的循环在阻塞式轮询中按键消抖后产生事件根据当前状态和事件查表得到下一个状态然后调用对应的绘制函数。菜单层级是主界面 → 心率监测 / 运动监测 / 历史记录 / 设置。在心率界面长按返回主界面短按切换子菜单。刚开始我用if-else写菜单逻辑写到第三层嵌套时已经晕了。后来统一改成状态机查表方式新增菜单项只是往表里加一行代码可维护性提升明显。4.2 FreeRTOS任务划分与优先级分配手环功能多起来之后裸机轮询就很吃力了心率采样要准时OLED刷新不能卡按键响应要快蓝牙要随时能收发。我引进了FreeRTOS把功能拆成四个任务。任务划分如下表任务名周期/触发方式优先级作用传感器采集任务10ms定时触发高读取MAX30102和MPU6050更新数据缓冲区算法处理任务100ms定时触发中执行计步、心率计算、卡路里推算显示刷新任务50ms定时触发低刷新OLED显示内容通信任务串口空闲中断唤醒中处理蓝牙指令回复数据包这里要说明一下优先级分配的原则传感器采样优先级最高因为漏采数据会导致心率波形断档算法处理可以容忍100ms的波动显示刷新最可以随意。通信任务和算法任务同为中优先级因为两者的实时性要求相近。信息流通过消息队列传递而不是用全局变量到处飞。这样做的理由是FreeRTOS任务切换时全局变量可能被抢占导致读到不完整数据。消息队列自带同步机制能保证数据不一致的问题从源头消失。4.3 低功耗策略与实测续航数据手环的续航是产品级项目必须考虑的。STM32F103的电量消耗主要来自三部分CPU运行功耗、传感器功耗、OLED背光。我把低功耗策略拆成三层。第一层是CPU降频与睡眠。空闲时让CPU进入__WFI等待事件中断实测电流从约20mA降到6mA。第二层是传感器断电。心率传感器不需要连续采集我设置成每分钟采30秒剩余30秒关闭MAX30102的LED这样电流能下降2mA左右。第三层是屏幕管理无操作10秒后自动熄屏抬手亮屏只维持5秒。实测结果OLED全亮且心率连续采样时系统电流约55mA正常佩戴的混合场景下平均电流约15mA400mAh电池实测续航26小时左右如果优化成每分钟采心率一次大约能到36小时。这个续航虽然比不上一线品牌的手环但对于DIY项目已经很满意了。如果想进一步压功耗换STM32L431并把系统时钟降频到16MHz预计还能再多撑一天。5. 常见问题排查与调试技巧5.1 I2C总线死锁的恢复方法I2C总线死锁是嵌入式开发中最常见也最头疼的问题之一。现象是程序跑到I2C通信时卡死写寄存器超时仿真器暂停后停在某个while循环里。原因通常是I2C总线的SDA线被某个从设备拉低主机无法控制总线产生停止条件。解决办法是手动把I2C总线恢复到空闲状态。方法是把SDA和SCL引脚配置为普通GPIO推挽输出模式手动产生9个以上时钟脉冲同时监测SDA直到SDA变为高电平再重新初始化I2C外设。我写了一个I2C_Bus_Recover()函数在每次I2C初始化前调用一次实测可以解决90%的死锁问题。但是根本解决要去查为什么死锁从设备电源没上电、从设备地址错误、I2C速率过快超过400kHz都会导致死锁。我排查后定位到是MAX30102模块的电源时序问题因为我把传感器电源接到了MCU的引脚控制为了低功耗上电时传感器还没就绪MCU就开始I2C通信导致总线异常。解决办法是上电后延时200ms再初始化传感器。5.2 STM32延时函数出问题的排查记录我调试时碰过延时函数卡死的情况调用HAL_Delay(100)后程序死循环。排查后发现是SysTick中断优先级设置问题。标准HAL库的HAL_Delay依赖SysTick中断如果外部中断的抢占优先级比SysTick低且中断没有及时退出SysTick永远得不到执行HAL_Delay就一直卡在while循环里。解决办法有三个把SysTick设置为最高抢占优先级或者在关键代码段关中断前不要调用延时函数或者用DWTData Watchpoint and Trace实现延时不依赖任何中断。我最终选了DWT方案在SystemInit后初始化DWT计数器重写一个delay_us稳定且不依赖外设。void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void DWT_Delay_Us(uint32_t us) { uint32_t cycles us * (SystemCoreClock / 1000000); uint32_t start DWT-CYCCNT; while ((DWT-CYCCNT - start) cycles); }5.3 ADC通道串扰问题与DMA内存对齐ADC多通道扫描有一个隐蔽问题如果相邻通道的采样保持时间设置太短前一个通道的残留电荷会影响后一个通道的采样值俗称通道串扰。现象是电池电压读数会随着温度传感器数值变化而跳动。解决方法是把采样时间调大。我最终用ADC_SampleTime_239Cycles5在72MHz时钟下约3.3微秒采样时间基本消除串扰。另外DMA缓冲区数组类型必须用uint16_t且对齐到2字节边界因为ADC的数据寄存器是16位。如果用uint32_t数组DMA搬运时会出现数据错位。5.4 STM32引脚复用的经验教训别跟调试口抢引脚手环PCB空间紧张我一度想用PB3、PB4作为普通IO去驱动OLED结果发现程序下载不进去系统一直报告找不到芯片。排查很久才反应过来STM32F103的PA13/PA14/PA15/PB3/PB4默认是SWD和JTAG调试引脚其中PA13/PA14是SWDIO/SWCLK如果把它们复用了调试器就无法连接。解决办法是在程序设计时先把这两个引脚配置成复用功能前连接到调试器禁用JTAG但保留SWD的函数或者干脆避让这些引脚。我最后选择了避让把OLED的DC脚从PA15挪到PA8彻底避开了调试口。5.5 FreeRTOS运行中优先级反转的教训FreeRTOS看似简单但优先级反转问题很容易被忽视。我遇到的现象是低优先级的算法处理任务和高优先级的传感器采集任务同时访问I2C总线时算法任务占着总线不释放传感器采集一直等待导致心率数据断流。解决办法有几种一是给I2C总线访问加互斥锁用xSemaphoreTake和xSemaphoreGive包裹二是分时访问不同任务不在同一时刻访问I2C三是把算法里对I2C的所有访问都挪进传感器采集任务里通过队列把数据吐出来。我最终选了第三种方式代码结构最清晰而且其他任务完全不碰I2C从机制上消灭了这个问题。6. 开发环境、固件升级与程序架构6.1 开发环境选择标准库、HAL库还是LL库这个问题每隔一段时间就有人吵一次。我的观点是做这种中小规模的量产型项目HAL库配合STM32CubeMX自动生成初始化代码是效率最高的。但前提是你得对HAL库的底层机制有基本认识不然遇到问题时会一头雾水。我自己的习惯是外设初始化交给CubeMX生成I2C、SPI、ADC、DMA、定时器这些配置繁琐的业务逻辑和算法部分用标准库风格写。刚开始用的是纯标准库毕竟本科课程就是标准库入门的但发现手动初始化ADC多通道DMA太容易漏配置切到HAL库后这部分工程量减少了一大半。还有一点值得提的是开发环境。我主力开发用的不是Keil而是VSCode加EIDE插件配合arm-none-eabi-gcc编译。好处是代码提示比Keil好太多而且GCC的编译警告更丰富能提前发现很多潜在问题。调试时用ST-Link和STM32CubeProgrammer刷固件烧录速度和稳定性都不错。Keil自然也能做但如果你有条件建议早点适应VSCode工作流。6.2 Bootloader与App分区的固件升级方案手环做好了之后固件升级一定是跑不掉的。用ST-Link直接烧录每次要拆后盖非常反人类所以我设计了IAPIn-Application Programming升级方案把STM32片内Flash拆成两块Bootloader区0x08000000~0x08003FFF16KB和App区0x08004000~0x0800FFFF48KB。Bootloader的功能很简单上电检查串口是否收到升级指令如果收到则进入接收固件模式把App区擦除、写入新固件否则跳转到App区执行。这里有两个踩坑点提醒大家一是跳转前一定要把系统时钟重新初始化因为Bootloader里初始化过的外设如UART在跳转后不会自动恢复二是App编译时要在Linker设置里把FLASH起始地址改成0x08004000否则App烧进去也没法运行。6.3 代码模块划分心得最后说说代码组织。手环项目代码量不算大约4000行C代码但如果全部塞在几个文件里后期改起来会想哭。我按功能模块拆成了main.c主流程、sensor_bsp.c传感器外设初始化、max30102.c心率传感器驱动、mpu6050.c运动传感器驱动、algorithm.c计步、心率算法、display.cOLED界面逻辑、ble_cmd.c蓝牙指令处理、power.c电源管理与低功耗控制。模块之间通过三个数据接口交互传感器数据缓冲区sensor_data_t结构体、显示消息队列、蓝牙命令队列。这样拆法的好处是如果你想换一款心率传感器只需要重写max30102.c并提供相同接口其他模块完全不用动。实际开发中我换了三款屏幕OLED驱动文件重写了一次其他代码一行没改。7. 实测效果与后续优化方向按照当前固件版本V1.3手环整体运行还是比较稳的。心率静息状态下和手头的商用指夹式血氧仪对比误差在±3bpm内血氧数值在95%~100%区间和参考设备偏差不超过2%计步准确率在正常步行条件下约95%跑步时约90%。睡眠监测目前只能区分清醒、浅睡、深睡三态对于REM快速眼动期这种更细的划分还在持续优化中。后续我计划做三个方向的升级一是算法层面增加基于HRV的压力评估功能二是硬件层面把F103换到STM32L431目标是续航翻倍三是交互层面把OLED换成低功耗TFT彩屏配合简单的LVGL界面库提升视觉体验。如果你要复刻这个项目我的建议是从最小可用版本起步先只做心率显示和计步两个功能跑通整条链路传感器采集→算法→显示再一步步加蓝牙、睡眠监测、低功耗等附加功能。这样即使某一环节卡住核心功能仍然能用也能保持心态不崩。整个项目从立项到V1.3大概花了六周时间其中近一半时间在调试传感器驱动和算法参数。说实话真正难的不是写代码而是调那一堆阈值和采样参数这需要耐心和大量的佩戴实测。希望这篇记录能帮你少走一些弯路。本文还有配套的精品资源点击获取