基于STM32的智能手环开发实战:从选型到低功耗任务调度

发布时间:2026/10/13 14:32:08
基于STM32的智能手环开发实战:从选型到低功耗任务调度 简介这份资源面向嵌入式初学者与单片机课程设计者提供一套基于STM32F103R8T6的智能手环完整实现方案涵盖硬件电路与软件编程两个层面。硬件部分集成ADXL345三轴加速度计用于计步与运动方向识别心率传感器实现实时心率采集温度传感器监测环境与皮肤温度LCD1602负责显示时间、步数、心率等数据软件部分涉及ADC、I2C、SPI等外设驱动配置以及传感器数据处理与界面刷新逻辑可配合Keil uVision或STM32CubeIDE开发。压缩包共139个文件约2.41MB以c与h源码、d与crf工程配置、o与hex编译产物为主另含uvprojx工程文件、map映射表及bat脚本目录结构完整便于直接编译与二次修改。目前已有8458人学习下载适合作为课程设计、毕业设计或嵌入式入门练手项目帮助读者快速理解多传感器融合与低功耗便携设备的开发流程。1. 一块 STM32 做的智能手环到底能跑出什么名堂很多人第一次听到“基于 STM32 单片机的智能手环”脑子里浮现的是某宝上几十块钱的成品。但真自己动手做一遍你会发现它是个极好的嵌入式综合练手项目一块 Cortex-M 内核的 MCU 同时要管 OLED 显示、心率血氧采集、六轴姿态解算、蓝牙数据上报和锂电池充放电管理资源调度、低功耗设计、传感器驱动一个都跑不掉。它适合已经点过 LED、调过串口想往“多外设协同 实时任务”方向进阶的开发者。我前后做过三版第一版屏幕闪烁、心率数据乱跳第二版待机不到半天第三版才算稳定。这篇就把这条路径完整拆开从选型到代码到踩坑让你少走我走过的弯路。2. 选型定架构为什么是 STM32 而不是别的2.1 主控型号怎么挑F103、F411 还是 L4 系列智能手环的核心矛盾是“算力够用”和“功耗够低”之间的拉扯。常见做法是在 STM32F103C8T6、STM32F411CEU6 和 STM32L431 之间选。F103 便宜、资料多72MHz 主频跑裸机任务调度够用但它没有硬件浮点单元做姿态解算时全是软件浮点功耗和速度都吃亏。F411 有 FPU100MHz 主频跑 Mahony 滤波或简单卡尔曼滤波很轻松代价是价格略高、封装偏小焊接难度大。L4 系列主打低功耗Stop 模式下能做到微安级但开发板生态不如 F103 丰富。我一般推荐新手从 F103C8T6 起步把整套逻辑跑通再换 F411 做性能升级。如果你一上来就冲着“戴在手上续航三天”去那直接上 L4 系列但要做好低功耗调试周期翻倍的准备。型号主频FPU典型运行电流适合阶段STM32F103C8T672MHz无约 30mA入门验证STM32F411CEU6100MHz有约 40mA性能进阶STM32L431CBT680MHz有约 15mA低功耗成品2.2 外设清单与接口分配一个能用的手环至少需要这几样0.96 寸 OLEDI2C 接口、MAX30102 心率血氧模块I2C 接口、MPU6050 六轴传感器I2C 接口、蓝牙模块UART 接口、锂电池加 TP4056 充电管理。你会发现三个传感器全是 I2C如果都挂在同一条总线上地址不冲突就没问题——MAX30102 默认地址 0x57MPU6050 默认 0x68OLED 常见 0x3C互不打架。但要注意 I2C 总线上的上拉电阻。很多模块自带上拉你并了三四个模块后等效上拉阻值变小上升沿变陡反而容易出现通信失败。血泪经验如果发现某个传感器时好时坏先拿示波器看 SCL/SDA 波形再考虑是不是上拉太强了。/* I2C 引脚分配以 STM32F103 的 I2C1 为例 */ #define OLED_I2C_ADDR 0x3C #define MAX30102_I2C_ADDR 0x57 #define MPU6050_I2C_ADDR 0x68 /* 初始化时逐个探测设备是否在线 */ uint8_t i2c_probe(uint8_t addr) { HAL_StatusTypeDef ret; ret HAL_I2C_IsDeviceReady(hi2c1, addr 1, 3, 100); return (ret HAL_OK) ? 1 : 0; }这段探测代码建议放在初始化最前面。逻辑很简单逐个发地址看有没有 ACK。参数里3是重试次数100是超时毫秒数。如果某个设备探测失败先别急着改代码用万用表量一下供电和上拉电压八成是硬件问题。2.3 电源方案锂电池 充电管理 稳压手环的电源设计直接决定你能不能把它戴出门。常见方案是 3.7V 锂电池接 TP4056 充电板输出经过 ME6211 或 XC6206 稳压到 3.3V 给 MCU 和传感器供电。这里有个容易翻车的点TP4056 的 BAT 输出和稳压芯片之间如果没加保护电池过放会直接损坏。我一般会在 BAT 和负载之间串一个 AO3401 PMOS 做防反接再加一个分压电阻到 ADC 引脚做电量检测。分压比按电池满电 4.2V、ADC 参考 3.3V 来算用 100K 和 200K 电阻分压实际到 ADC 的电压约 2.8V留了余量。/* 电池电压采集与百分比估算 */ #define ADC_REF_VOLTAGE 3.3f #define DIVIDER_RATIO 3.0f /* (100K 200K) / 100K */ #define BAT_FULL 4.2f #define BAT_EMPTY 3.3f float get_battery_voltage(uint16_t adc_raw) { float v_adc (adc_raw / 4095.0f) * ADC_REF_VOLTAGE; return v_adc * DIVIDER_RATIO; } uint8_t get_battery_percent(float voltage) { if (voltage BAT_FULL) return 100; if (voltage BAT_EMPTY) return 0; return (uint8_t)((voltage - BAT_EMPTY) / (BAT_FULL - BAT_EMPTY) * 100); }注意DIVIDER_RATIO要按你实际用的电阻算别照抄。ADC 读数建议做 8 次平均再代入否则百分比会跳来跳去用户看着像坏了。3. 传感器驱动与数据采集让心率血氧不飘3.1 MAX30102 的初始化与 FIFO 读取MAX30102 是手环里最娇气的传感器。它对佩戴松紧、环境光、手指抖动都极其敏感。初始化流程必须严格按数据手册来先复位再配置 FIFO再设模式最后启动。顺序错了它就不出数。/* MAX30102 关键寄存器配置 */ #define REG_MODE_CONFIG 0x09 #define REG_SPO2_CONFIG 0x0A #define REG_LED1_PA 0x0C #define REG_LED2_PA 0x0D #define REG_FIFO_WR_PTR 0x04 #define REG_FIFO_RD_PTR 0x06 #define REG_FIFO_DATA 0x07 void max30102_init(void) { max30102_write(REG_MODE_CONFIG, 0x40); /* 复位 */ HAL_Delay(100); max30102_write(REG_FIFO_WR_PTR, 0x00); max30102_write(REG_FIFO_RD_PTR, 0x00); /* SpO2 模式采样率 100Hz脉宽 411usADC 范围 4096 */ max30102_write(REG_SPO2_CONFIG, 0x27); /* LED 电流决定信号幅度 */ max30102_write(REG_LED1_PA, 0x24); max30102_write(REG_LED2_PA, 0x24); max30102_write(REG_MODE_CONFIG, 0x03); /* SpO2 模式启动 */ }REG_SPO2_CONFIG写 0x27 的含义是ADC 满量程 4096nA采样率 100 次每秒LED 脉宽 411 微秒。REG_LED1_PA和REG_LED2_PA写 0x24 对应约 7.2mA 驱动电流。如果你发现信号太弱先加大这两个值到 0x3F 试试如果波形削顶就往下调。这个参数没有万能值跟你的外壳透光率和佩戴位置强相关。3.2 心率计算从原始 PPG 到 BPM拿到 FIFO 里的原始数据后不能直接当心率用。PPG 信号里混着直流分量、呼吸基线漂移和运动伪影。我一般用“直流滤波 峰值检测”这套组合拳计算量小在 F103 上跑毫无压力。#define PPG_BUF_SIZE 100 static uint32_t ppg_buf[PPG_BUF_SIZE]; static uint8_t ppg_idx 0; /* 滑动平均估直流分量 */ uint32_t ppg_dc_estimate(void) { uint32_t sum 0; for (int i 0; i PPG_BUF_SIZE; i) { sum ppg_buf[i]; } return sum / PPG_BUF_SIZE; } /* 峰值检测计算 BPM */ uint8_t calc_heart_rate(void) { static uint32_t last_peak_time 0; static uint8_t peak_count 0; uint32_t dc ppg_dc_estimate(); uint32_t threshold dc (dc 3); /* 直流分量的 1.125 倍 */ for (int i 1; i PPG_BUF_SIZE - 1; i) { if (ppg_buf[i] threshold ppg_buf[i] ppg_buf[i-1] ppg_buf[i] ppg_buf[i1]) { uint32_t now HAL_GetTick(); if (now - last_peak_time 300) { /* 300ms 不应期 */ peak_count; last_peak_time now; } } } /* 每 10 秒统计一次 */ uint8_t bpm peak_count * 6; peak_count 0; return bpm; }阈值取直流分量的 1.125 倍是个经验值对应约 12.5% 的交流波动。不应期设 300ms 是为了防止把同一个脉搏波的二次谐波误判成两次心跳。如果你发现静止时心率挺准、一动就乱飞那说明运动伪影没处理要么加加速度计做自适应滤波要么直接告诉用户“请保持静止”。3.3 MPU6050 姿态解算与计步逻辑MPU6050 出的是三轴加速度和三轴角速度。计步不需要完整姿态解算用加速度模值的峰值检测就够了。但如果你想做抬腕亮屏那就得算俯仰角和横滚角。/* 加速度模值计步 */ #define STEP_THRESHOLD_HIGH 1.3f /* 单位g */ #define STEP_THRESHOLD_LOW 0.8f #define STEP_DEBOUNCE_MS 250 uint16_t step_count 0; void step_detector(float ax, float ay, float az) { static uint8_t step_state 0; static uint32_t last_step_tick 0; float mag sqrtf(ax*ax ay*ay az*az); if (step_state 0 mag STEP_THRESHOLD_HIGH) { uint32_t now HAL_GetTick(); if (now - last_step_tick STEP_DEBOUNCE_MS) { step_count; last_step_tick now; } step_state 1; } else if (step_state 1 mag STEP_THRESHOLD_LOW) { step_state 0; } }STEP_THRESHOLD_HIGH和STEP_THRESHOLD_LOW构成迟滞比较器防止在阈值附近抖动导致多计。STEP_DEBOUNCE_MS设 250ms 对应最快约 4 步每秒正常人走路达不到这个频率。如果你发现跑步时计步偏少把高阈值降到 1.2 试试如果手晃动就计步那就把高阈值往上提。4. 避坑与排查那些让我熬夜的瞬间4.1 OLED 显示闪烁或花屏现象屏幕内容每隔几秒闪一下或者出现随机噪点。原因通常有两个一是 I2C 通信速率太高F103 默认 100kHz 在长排线上容易出错二是刷新时没有用局部刷新整屏重写导致视觉闪烁。解决方法是把 I2C 速率降到 100kHz 以下同时改用页地址局部刷新只更新变化的区域。另外OLED 的 VCC 和 GND 一定要就近加 100nF 去耦电容这个坑我踩了两次。4.2 心率数据一直为零或满量程现象MAX30102 读出来全是 0 或者全是 0x3FFFF。原因FIFO 指针没有正确复位或者 I2C 读写时序不匹配。解决初始化时务必先写REG_FIFO_WR_PTR和REG_FIFO_RD_PTR为 0再启动采集。如果还是不行用逻辑分析仪抓 I2C 波形看 ACK 位是否正常。另外注意 MAX30102 的 INT 引脚如果没接就别用中断方式读老老实实轮询。4.3 蓝牙连接后数据丢包现象手机端收到的数据时断时续或者干脆连不上。原因UART 波特率和蓝牙模块不匹配或者 MCU 在发送时被其他中断打断导致时序错乱。解决先把波特率统一到 9600 或 115200然后在发送函数里加临界区保护。如果用的是 HC-05 这类模块注意它的 STATE 引脚要接 MCU 一个 GPIO用来判断是否已连接否则 MCU 会一直往空气里发数据。4.4 电池电量显示跳变现象电量百分比在 60% 到 80% 之间反复横跳。原因ADC 采样没有滤波或者分压电阻精度不够。解决ADC 连续采 16 次去掉最大最小再平均分压电阻用 1% 精度的。另外锂电池的电压和剩余电量不是线性的3.7V 以下掉得特别快如果你要做得精细得建一张电压-电量查找表。4.5 整机功耗降不下来现象充满电只能用四五个小时。原因MCU 没进低功耗模式传感器一直全速运行OLED 常亮。解决在无操作时把 MCU 切到 Stop 模式用 RTC 或外部中断唤醒MAX30102 改成间歇采样比如每 10 秒采 2 秒OLED 设置 30 秒无操作自动熄屏。这三招下来续航能从半天拉到两天以上。5. 低功耗与任务调度让手环真正能戴出门5.1 用 SysTick 状态机替代 delay 阻塞新手最容易犯的错是到处HAL_Delay。一旦 delay 起来整个系统什么都干不了。正确做法是用 SysTick 做一个毫秒级时基然后每个任务用状态机驱动。/* 简易任务调度器 */ typedef struct { void (*task)(void); uint32_t period_ms; uint32_t last_tick; } task_t; task_t tasks[] { {task_display, 100, 0}, {task_ppg_read, 10, 0}, {task_step_calc, 20, 0}, {task_ble_send, 500, 0}, }; void scheduler_run(void) { uint32_t now HAL_GetTick(); for (int i 0; i sizeof(tasks)/sizeof(task_t); i) { if (now - tasks[i].last_tick tasks[i].period_ms) { tasks[i].task(); tasks[i].last_tick now; } } }这个调度器在main的while(1)里循环调用每个任务按自己的周期执行互不阻塞。task_display100ms 刷一次task_ppg_read10ms 读一次 FIFOtask_ble_send500ms 上报一次。参数你可以按实际需求调但注意别把高频任务设得太密否则 MCU 一直在跑低功耗就无从谈起。5.2 Stop 模式唤醒与 RTC 定时当所有任务都空闲超过一定时间就让 MCU 进 Stop 模式。唤醒源用 RTC 闹钟每 1 秒醒一次检查有没有事件。void enter_stop_mode(void) { /* 关闭外设时钟 */ __HAL_RCC_GPIOA_CLK_DISABLE(); __HAL_RCC_GPIOB_CLK_DISABLE(); /* 配置 RTC 1 秒后唤醒 */ HAL_RTC_SetAlarm_IT(hrtc, alarm_time, RTC_FORMAT_BIN); /* 进 Stop 模式 */ HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); /* 唤醒后重新配置时钟 */ SystemClock_Config(); }注意进 Stop 前要把不用的 GPIO 设成模拟输入否则漏电流会吃掉你的续航。唤醒后时钟会切回 HSI必须重新调 SystemClock_Config不然串口波特率全乱。这个坑我在第二版上栽过蓝牙直接变砖。5.3 传感器间歇采样策略心率不需要一直采。我的做法是正常佩戴时每 10 秒采 2 秒抬腕亮屏时切到连续采样。这样 MAX30102 的平均功耗能降到原来的五分之一。实现上用一个状态变量控制采样窗口窗口内正常读 FIFO窗口外把 MAX30102 设成关断模式。void ppg_sampling_control(void) { static uint32_t cycle_start 0; uint32_t now HAL_GetTick(); uint32_t elapsed now - cycle_start; if (elapsed 2000) { max30102_set_mode(0x03); /* SpO2 模式 */ task_ppg_read(); } else if (elapsed 10000) { max30102_set_mode(0x80); /* 关断 */ } else { cycle_start now; } }0x80是 MAX30102 的关断命令写进REG_MODE_CONFIG即可。这个策略对续航提升非常明显代价是心率更新频率变低但手环本来就不需要秒级心率。6. 进阶技巧把裸机方案推到更稳的边界如果你已经跑通了上面所有环节接下来可以往三个方向深挖。第一是给 PPG 信号加一个简单的带通滤波截止频率设 0.5Hz 到 5Hz能把呼吸漂移和大部分运动伪影压下去。实现上用二阶 IIR 就够了F103 跑起来毫无压力。第二是把蓝牙协议从透传改成自定义的帧格式加帧头、长度、校验和手机端解析起来更可靠。第三是做一个简单的数据缓存蓝牙断开时把数据存到 MCU 的 Flash 里连上后再补传。/* 简易 IIR 带通滤波用于 PPG 信号 */ float ppg_bandpass(float input) { static float x[3] {0}, y[3] {0}; /* 系数按 100Hz 采样率、0.5-5Hz 通带估算 */ const float b0 0.2929f, b1 0.0f, b2 -0.2929f; const float a1 -1.4142f, a2 0.4142f; x[0] input; y[0] b0*x[0] b1*x[1] b2*x[2] - a1*y[1] - a2*y[2]; x[2] x[1]; x[1] x[0]; y[2] y[1]; y[1] y[0]; return y[0]; }这组系数是我用在线滤波器设计工具算的你可以按自己的采样率重新生成。注意y数组要初始化成 0否则前几个输出会飞。滤波后的信号再送进峰值检测心率稳定性会明显提升。最后说一个我自己的习惯每改一版硬件先不写业务代码而是写一个测试固件把每个传感器单独跑一遍串口打印原始数据。确认硬件没问题了再往上叠逻辑。这个习惯帮我省了至少两周的调试时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询