STM32智能手环开发:心率计步体温多传感融合实现

发布时间:2026/9/16 3:30:20
STM32智能手环开发:心率计步体温多传感融合实现 简介这是一份基于STM32单片机的智能手环完整设计项目面向嵌入式初学者、电子竞赛备赛者以及可穿戴设备开发者。项目以STM32为控制核心整合脉搏心率检测、计步器、体温显示等功能涉及传感器选型与数据处理、外设驱动、显示交互、电源管理和系统集成能够帮助读者建立从硬件搭建到固件实现的全链路认知。压缩包为zip格式大小约66.24MB站内暂未展示具体文件明细下载后可直接用于项目学习与二次开发。已有176人学习适合用于STM32课程设计、毕业设计或相关技能进阶。参考该项目读者可重点理解PPG心率信号滤波与心率计算、加速度计步态识别与计步逻辑、体温采集与显示处理以及低功耗UI交互等关键模块的设计思路从而为独立开发智能手环类可穿戴设备打下坚实基础。1. 为什么智能手环项目还在用STM32而不是蓝牙SoC一个基于STM32单片机做的智能手环资源把脉搏心率、计步器和体温显示集成在同一块板子上适合毕设、课设和想自己写传感器算法的工程师。很多人第一反应是既然做手环为什么不直接用NRF52832或国产BLE SoC但在这个场景下STM32生态成熟、资料密度高从标准库新建工程到串口调试都有现成模板传感器接口用I2C/SPI/ADC就能覆盖不需要在蓝牙协议栈上消耗时间。这个项目正好把PPG心率检测、加速度计步数识别、温度采集和OLED显示串成一条完整链路调通之后可以迁移到更多STM32物联网终端上。源码里最值得看的不是某个外设的用法而是三类传感器数据怎么在同一个内核里协调工作。2. 系统硬件搭建STM32外设分配与三类传感器接口设计在这个资源里STM32的核心工作是把三组传感器数据定时读出来再送给OLED和串口。常见做法用STM32F103C8T6资源足够外设分配可以按下面的思路来心率模块和OLED共享一组I2C加速度计单独走另一组I2C或SPI温度传感器如果选NTC则接ADC如果选红外热电堆则挂在I2C上。这样Pin脚不打架代码结构也清晰。2.1 外设资源规划与 CubeMX 初始化F103C8T6有3个USART、2个I2C、2个SPI、1个12位ADC做手环绰绰有余。我一般会让I2C1跑400kHz快速模式接MAX30102心率模块和OLEDI2C2接加速度计ADC1接NTC分压电路TIM2做5ms定时采样中断USART1用作日志输出。这样心率采样和步数采样可以统一挂在同一个中断节拍里避免各自使用delay导致时序乱掉。外设总线/通道建议引脚用途I2C1400kHzPB6/PB7MAX30102心率、OLED显示I2C2400kHzPB10/PB11ADXL345/MPU6050加速度计ADC1IN0PA0NTC体温检测TIM2定时中断无5ms采样节拍USART1115200PA9/PA10串口调试初始化代码可以直接用CubeMX生成核心动作如下void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz 快速模式 hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }ClockSpeed 400000是快速模式I2C心率模块和OLED都能支持跑100kHz也不会出错但读取FIFO时耗时更长。AddressingMode用7位地址MAX30102的默认地址是0x57OLED一般是0x3C或0x78地址冲突时需要在设备地址上加偏移。I2C初始化后建议先用HAL_I2C_IsDeviceReady()探测设备是否存在避免后续读寄存器时卡在总线等待上。2.2 心率传感器选型PPG光电容积描记与MAX30102心率检测最常用的是光电容积描记PPG方案MAX30102把红光LED、红外LED、光电二极管和ADC都封在一个小模块里外部只需要一组I2C和两个去耦电容。它的FIFO可以缓存32个采样点适合让STM32用中断方式批量读取而不是每个采样点都打断主循环。选型时要注意模块的工作电压。MAX30102的VDD是1.8V但很多现成模块已经带了电平转换可以直接用3.3V供电。如果资源里用的是MAX30100寄存器映射略有差异移植时要先看头文件里的地址定义。PPG信号对手指按压力度很敏感代码写得再好佩戴松紧不合适也会导致基线漂移这一点在后面的滤波章节会继续展开。2.3 计步加速度计接口与放置方向计步器选ADXL345或MPU6050都可以资源里如果只实现计步ADXL345更省电规格书上待机电流在0.1µA级别比MPU6050整机更适合穿戴设备。加速度计建议放在手腕的背侧也就是表盘正下方X轴沿手臂方向Z轴垂直手背。这样走路时手臂摆动带来的加速度分量在X轴和Z轴上都能体现合加速度波形里会出现明显的波峰。配置加速度计时量程选±2g就够因为走路摆臂的峰值通常在1.2g到1.8g之间。如果量程选±16g分辨率会被浪费信号的小波动会被量化噪声吃掉。加速度采样率设20Hz即可不需要用到传感器内部的更高采样率因为STM32端还要做阈值判断用20Hz可以减少CPU开销。2.4 体温传感器NTC与红外热电堆的取舍体温显示有两种主流方案NTC热敏电阻和红外热电堆。资源里如果只显示温度数字NTC方案成本低、接线简单但响应速度慢受环境温度影响大红外热电堆用MLX90614这类芯片非接触测量精度能做到±0.5℃但价格贵而且需要校准发射率。传感器方案接口精度响应速度典型场景NTC10kΩ/3950BADC±0.5~1℃校准后2~5秒低成本体温手环MLX90614I2C±0.5℃1秒非接触测温模块从资源标题的“体温显示”来看NTC实现的可能性更高因为源码里只需要一个ADC_GetValue()加B值公式。选MLX90614时I2C地址是0x5A读取RAM地址0x07就能拿到被测物温度。无论用哪种都要在首次上电后做一次软件校准后面第4章会给出具体补偿方式。3. 固件实现心率、步数与温度的数据计算流程传感器初始化只是第一步真正决定手环能不能用的是数据计算流程。心率、步数和体温三路信号特征完全不同心率是周期性波形步数是冲击型波形体温是缓变信号。放在同一个中断里处理时要注意数据类型的长度和溢出边界。3.1 心率计算从 PPG 原始值到 BPMPPG信号经过传感器内部ADC后进入STM32的是一串16位原始值。心率的计算思路是先找波峰再算两个波峰之间的时间间隔最后用60除以平均间隔得到每分钟心率BPM。找波峰不能用固定绝对阈值因为手指肤色、环境光和佩戴压力都会改变基线但可以用“信号上升沿过阈值”的简化方法。#define SIGNAL_THRESHOLD 3500 #define MIN_PEAK_INTERVAL_MS 300 uint32_t last_beat_ms 0; uint16_t last_value 0; uint8_t sample_ppg(uint16_t val, uint32_t now_ms) { uint8_t beat_detected 0; // 连续两个采样点从阈值下方穿越到上方判定为一个波峰 if (val SIGNAL_THRESHOLD last_value SIGNAL_THRESHOLD (now_ms - last_beat_ms) MIN_PEAK_INTERVAL_MS) { beat_detected 1; last_beat_ms now_ms; } last_value val; return beat_detected; }SIGNAL_THRESHOLD需要根据实际读取到的PPG波形调整静息时信号幅度通常在2000到5000之间如果灯光过强会饱和到4095以上。MIN_PEAK_INTERVAL_MS是防止将同一个波峰的高频抖动误判成两次心跳300ms对应最高200BPM这也是可穿戴设备的常见上限。实际工程里不会只用一次上升沿而是会维护一个最近5个峰值间隔的滑动数组去掉最大值和最小值后取平均得到的心率更稳。3.2 计步检测合加速度阈值与时间窗计步器用三轴加速度的合幅值最直接因为无论手环怎么转走路时身体上下起伏都会让合加速度产生周期性波动。把三个轴先做平方和再开方得到一个以重力加速度g为中心的标量信号然后在这个信号上做阈值穿越。#define STEP_THRESHOLD 1.2f // 合加速度阈值为1.2g #define STEP_MIN_INTERVAL_MS 250 // 最小步间隔250ms float last_mag 1.0f; uint32_t last_step_ms 0; uint8_t process_acc(float x, float y, float z, uint32_t now_ms) { float mag sqrtf(x * x y * y z * z); uint8_t step 0; // 合加速度从低于1.2g跃升到高于1.2g判定为一步 if (mag STEP_THRESHOLD last_mag STEP_THRESHOLD (now_ms - last_step_ms) STEP_MIN_INTERVAL_MS) { step 1; last_step_ms now_ms; } last_mag mag; return step; }这里的STEP_THRESHOLD用1.2g是因为静止时合加速度稳定在1.0g左右走路时摆动峰值会超过1.2g。如果手臂摆动幅度小可以把阈值降到1.1g但幅度小也意味着运动强度低降到1.05g后容易把走路时的手腕抖动也算成步数。STEP_MIN_INTERVAL_MS设为250ms对应最快步频240步/分钟正常人极限步频不会超过这个值。加速度计原始值要除以灵敏度换算成gADXL345在±2g量程下的灵敏度是256LSB/gMPU6050是16384LSB/g换算漏掉的话阈值判断会完全错位。3.3 温度转换NTC ADC读值与MLX90614读取如果用NTC温度计算依靠电阻-温度关系。常见电路是10kΩ NTC与10kΩ普通电阻串联分压ADC读到的电压比例直接对应NTC的电阻值。转换公式如下。#define ADC_FULL_SCALE 4095.0f #define BALLAST_RES 10000.0f #define B_VALUE 3950.0f #define T0_K 298.15f #define R0 10000.0f float read_ntc_temp(uint16_t adc) { // 先由ADC值算分压再求NTC电阻 float ratio (float)adc / ADC_FULL_SCALE; float rntc BALLAST_RES * (ratio / (1.0f - ratio)); // 用B值公式将电阻转成开尔文温度 float temp_k 1.0f / (1.0f / T0_K logf(rntc / R0) / B_VALUE); return temp_k - 273.15f; }ADC_FULL_SCALE必须和STM32的ADC位数一致F103是12位所以用4095。BALLAST_RES是分压电阻与R0相等时分压点最接近中间值ADC分辨率利用率最高。B值不是常数在25℃到50℃之间用3950K误差不大体温场景刚好在这个区间。如果资源里用的是MLX90614读取要简单得多I2C读0x07寄存器得到16位原始值温度等于raw * 0.02f - 273.15f。需要特别注意的是MLX90614返回的是开尔文温度直接乘以0.02后要减去273.15否则显示出来会多出两百多度。4. 数据滤波与调参噪声、抖动和热惯性怎么处理原始信号直接上算法会很难看。PPG信号有环境光噪声步数信号有手腕抖动温度信号有热惯性。这一章解决的是“读数是出来了但不稳定”的问题。4.1 滑动平均滤波与心率窗口PPG信号最容易受到50Hz工频和谐波干扰同时走路时的手环晃动也会叠加低频噪声。常见做法是先用滑动平均压掉高频毛刺再做阈值判断。滑动平均窗口越小响应越快但滤波效果弱窗口越大波形越平滑但波峰会变钝导致峰值位置滞后。#define FILTER_WIN 8 uint32_t win_sum 0; uint16_t win_buf[FILTER_WIN] {0}; uint8_t win_idx 0; uint16_t moving_average(uint16_t new_sample) { win_sum - win_buf[win_idx]; win_sum new_sample; win_buf[win_idx] new_sample; win_idx (win_idx 1) % FILTER_WIN; return (uint16_t)(win_sum / FILTER_WIN); }这段代码用环形缓冲区维护最近8个采样点的累加和每次新增采样时先减去最旧值再加上新值最后取平均。避免每次重复累加全部数据省掉一个循环。如果采样率是100Hz窗口8对应80ms这个时长不会明显压低心率有效频段。窗口再加大到32时峰谷会被抹平心率检测的上升沿也会变缓容易漏掉快速连续心跳。4.2 步数阈值自适应与抖动抑制计步阈值固定不动的方案在实验室里没问题但手臂大幅甩动时合加速度峰值可能到2g以上走路轻缓时只有1.15g。更稳的做法是在固定阈值基础上再做一个短时最大值跟踪用“动态阈值时间窗”双条件确认。场景合加速度峰值推荐阈值最小步间隔慢走1.15~1.3g1.10g300ms正常走路1.3~1.6g1.20g250ms跑步1.8~2.5g1.35g180ms调整时不要只盯着步数总数要看波形本地是否连续。跑步时步频高STEP_MIN_INTERVAL_MS如果设成250ms会漏掉一部分快速步慢走时把250ms改短则容易把一次落地后的两次抖动算成两步。实践里可以先在串口里打印合加速度波形肉眼确认一个摆动周期内只有一个波峰再反推阈值。4.3 体温热惯性与软件补偿NTC和红外热电堆都有热惯性。NTC因为自身封装热容量大从环境温度进入人体表面需要几秒到十几秒红外热电堆虽然响应快但手环佩戴角度变化后辐射面不同读数也会漂。资源里如果只做单次ADC转换就直接显示温度会跳变一两度。常见补偿方法是连续采集N次后去掉最大值和最小值再取平均。体温变化很缓采样周期可以放长到500ms取10次平均相当于5秒内平滑一次效果好于直接在显示层做滤波。另一个技巧是做软件偏移校准先用体温计实测口腔温度再对比手环显示值算出差值后写死在配置区里。比如显示35.8℃实测36.5℃偏移量就是0.7℃。注意这个偏移只在相同佩戴位置有效换手佩戴后要重新校准。5. 用串口波形标定阈值并把手环功耗降下来很多源码能跑通但读数不准原因是阈值和滤波参数没针对硬件调试。这一章给一个可复现的调参方法以及降低功耗的常见手段。5.1 用串口把原始数据拉出来调参不要用OLED看波形OLED刷新率低调参效率很差。把PPG原始值和合加速度值通过串口发到PC上用串口绘图工具看波形阈值改起来要快得多。在工程里重定向printf到USART1然后打印关键数据。int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 10); return ch; } printf(t:%lu ppg:%u mag:%d.%03d temp:%d.%d\r\n, (unsigned long)now_ms, (unsigned int)filtered_ppg, (int)mag, (int)((mag - (int)mag) * 1000), (int)temp, (int)((temp - (int)temp) * 10));fputc重定向后所有printf输出都会走USART1。格式里mag:%d.%03d把合加速度的整数和小数拆开打印方便在绘图工具里直接画成浮点波形。我用这样的方式调试时一般先让手环静止2分钟确认PPG基线稳定再快走100步看合加速度波形里波峰是否清晰。波形出不来就把SIGNAL_THRESHOLD往下调波形毛刺太多就加大滑动平均窗口。5.2 低功耗模式与显示刷新策略手环电池不大如果STM32一直全速跑功耗很难看。低功耗的核心是让CPU在两次采样之间睡过去而不是用HAL_Delay()空转。用TIM2定时中断唤醒中断里完成传感器读取和算法主循环只负责显示和串口输出。进入停止模式后的运行逻辑如下。while (1) { // 进入停止模式等待TIM2或外部中断唤醒 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config(); // 退出停止模式后需要重新配置时钟 OLED_Refresh(); // 只在有采样事件时才刷新 }PWR_LOWPOWERREGULATOR_ON表示低功耗调压器模式唤醒时间稍长但电流更低。PWR_STOPENTRY_WFI是等中断唤醒TIM2每5ms唤醒一次对心率100Hz采样来说足够。唤醒后必须调用SystemClock_Config()否则外设时钟还停在之前的低频状态I2C通信会异常。显示部分不要每帧都刷新整屏OLED只在心率、步数或温度变化超过显示精度时才刷新OLED大面积刷新在5V手环上能占到一半功耗。这样把系统时钟降到48MHz、传感器分时供电后整机电流可以稳定在5mA以内。本文还有配套的精品资源点击获取