简介:本资源是一套基于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,内存地址自增;启动后不用管CPU,DMA会自动把两路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); }温度传感器内部校准值存在地址0x1FFFF7E8(F103),读取后结合当前ADC值计算,公式是T = (V25 - Vdda * ADC_value / 4096) / Avg_Slope + 25,其中V25约1.43V,Avg_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;正常佩戴的混合场景下平均电流约15mA,400mAh电池实测续航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设置为最高抢占优先级;或者在关键代码段关中断前不要调用延时函数;或者用DWT(Data 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直接烧录每次要拆后盖,非常反人类,所以我设计了IAP(In-Application Programming)升级方案,把STM32片内Flash拆成两块:Bootloader区(0x08000000~0x08003FFF,16KB)和App区(0x08004000~0x0800FFFF,48KB)。
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.c(OLED界面逻辑)、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大概花了六周时间,其中近一半时间在调试传感器驱动和算法参数。说实话,真正难的不是写代码,而是调那一堆阈值和采样参数,这需要耐心和大量的佩戴实测。希望这篇记录能帮你少走一些弯路。
本文还有配套的精品资源,点击获取