STM32智能手环开发:心率计步体温多传感融合实现
2026/9/16 3:30:15 网站建设 项目流程

简介:这是一份基于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心率模块和OLED;I2C2接加速度计;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的默认地址是0x57,OLED一般是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℃,但价格贵,而且需要校准发射率。

传感器方案接口精度响应速度典型场景
NTC(10kΩ/3950B)ADC±0.5~1℃(校准后)2~5秒低成本体温手环
MLX90614I2C±0.5℃1秒非接触测温模块

从资源标题的“体温显示”来看,NTC实现的可能性更高,因为源码里只需要一个ADC_GetValue()加B值公式。选MLX90614时,I2C地址是0x5A,读取RAM地址0x07就能拿到被测物温度。无论用哪种,都要在首次上电后做一次软件校准,后面第4章会给出具体补偿方式。

3. 固件实现:心率、步数与温度的数据计算流程

传感器初始化只是第一步,真正决定手环能不能用的是数据计算流程。心率、步数和体温三路信号特征完全不同:心率是周期性波形,步数是冲击型波形,体温是缓变信号。放在同一个中断里处理时,要注意数据类型的长度和溢出边界。

3.1 心率计算:从 PPG 原始值到 BPM

PPG信号经过传感器内部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步/分钟,正常人极限步频不会超过这个值。加速度计原始值要除以灵敏度换算成g,ADXL345在±2g量程下的灵敏度是256LSB/g,MPU6050是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以内。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询