拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于STM32的低功耗DIY智能手表实战:硬件选型、软件架构与功耗优化

1. 为什么我要用 STM32 从头撸一块手表第一次动了做手表的念头是看到市面上那些动辄上千的智能手表功能确实花哨但续航普遍拉胯一天一充甚至半天一充。我当时就想能不能用 STM32 做一块“够用就好”的手表——能看时间、能计步、能测个心率、续航撑个三五天关键是所有代码和硬件都攥在自己手里想改什么改什么。这个想法一旦冒出来就压不住了前后折腾了大概两个月中间踩了无数坑从选芯片到画板子、从驱动屏幕到调 RTC 走时精度每一步都有故事。STM32 做手表这件事听起来像是“用大炮打蚊子”毕竟一块手表的核心功能无非就是走时。但真正动手之后你会发现STM32 的丰富外设恰恰是它的优势所在RTC 负责走时、TIM 做按键扫描和 PWM 背光、SPI 驱动屏幕、I2C 挂传感器、ADC 读电池电量、DMA 搬运数据降低 CPU 占用、Stop 模式把功耗压到微安级。这些外设如果单独拿出来都不难难的是怎么让它们协同工作还要在有限的电池容量下跑出可接受的续航。这篇文章适合谁看如果你已经玩过 STM32 的基础例程点过灯、调过串口、用过定时器那这篇内容就是为你准备的。我会把整个手表的开发过程拆开揉碎从硬件选型到软件架构从屏幕驱动到功耗优化每一步都讲清楚“为什么这么做”而不只是“怎么做”。如果你是完全零基础的小白建议先找块最小系统板把 GPIO、定时器、中断这些基础概念过一遍再回来不然有些地方可能会觉得跳跃。需要提前说明的是我做这块手表的目标是“实用优先”不是追求极致轻薄或者炫酷 UI。所以选型上会偏向于好焊接、好调试、资料多的方案而不是最新最贵的芯片。整个项目的核心思路是用成熟的方案快速跑通再逐步优化细节。下面就从硬件选型开始一步步展开。2. 硬件选型主控、屏幕、传感器怎么挑2.1 主控芯片为什么我最终选了 STM32L4 系列选主控是第一个岔路口。STM32 家族庞大F1、F4、L4、G0、G4 各有各的定位。我一开始用的是手头现成的 STM32F103C8T6 最小系统板就是大家常说的“蓝板”。这块板子便宜、资料多、社区活跃拿来跑通原型完全没问题。但真正要做到手表形态F103 有两个硬伤一是功耗偏高运行模式下的电流在几十毫安级别就算进 Stop 模式也有几十微安对于一块 200mAh 的小电池来说续航会很紧张二是封装偏大LQFP48 虽然不算大但放在手表 PCB 上还是占地方。后来我把目光转向了 STM32L4 系列具体型号是 STM32L431CBT6。选它的理由很直接Cortex-M4 内核带 FPU主频 80MHz性能够用LQ 系列主打低功耗运行模式约 100μA/MHzStop 2 模式可以低到 1μA 左右RTC 走时加上 LCD 刷新整体平均电流可以控制在 200μA 以内LQFP48 封装和 F103 兼容PCB 改动小Flash 128KB、RAM 64KB对于手表固件来说绰绰有余。这里有个细节值得展开说L4 系列的 Stop 模式分 Stop 0、Stop 1、Stop 2 三档功耗依次降低但唤醒时间和保留的外设也不同。Stop 2 最省电但只有 RTC、IWDG、部分 GPIO 能唤醒SRAM2 可以保留数据。我的做法是把走时相关的变量放在 SRAM2主循环进 Stop 2靠 RTC 闹钟每秒唤醒一次刷新时间按键用 EXTI 唤醒。这样大部分时间芯片都在微安级待机只有刷新屏幕和读传感器时才短暂跑到运行模式。如果你手头只有 F103 也不想换其实也能做只是续航会打折扣。我的建议是原型阶段用 F103 跑通逻辑正式版再换 L4。这样既不耽误学习也能保证最终效果。2.2 显示屏SPI 屏还是 I2C 屏这是个问题手表屏幕的选择直接决定了 UI 能做成什么样。市面上常见的小尺寸屏幕有几种0.96 寸 OLEDSSD1306、1.3 寸 OLEDSH1106、1.14 寸 IPSST7789、1.28 寸圆屏GC9A01。我前后试过三种最后留用的是 1.28 寸圆屏 GC9A01240x240 分辨率SPI 接口。先说 OLED。SSD1306 的 0.96 寸屏是最容易上手的I2C 接口两根线搞定驱动库满地都是。但它有两个问题一是尺寸小做手表表盘显得局促二是 OLED 长时间显示静态内容会有烧屏风险虽然手表 UI 会经常刷新但表盘边框这类固定元素还是有隐患。SH1106 的 1.3 寸屏稍大一些但本质还是 OLED烧屏问题依旧。再说 IPS 圆屏。GC9A01 这块屏是我最后选定的理由如下240x240 的分辨率在 1.28 寸上像素密度够高显示细腻圆形屏幕天然适合手表形态不用额外做遮罩SPI 接口虽然线多几根但刷新率高配合 DMA 可以做到流畅刷屏IPS 可视角度好侧看也不偏色。唯一的缺点是驱动比 OLED 复杂一些初始化序列长但网上有现成的开源驱动可以参考。这里有个实操心得GC9A01 的 SPI 时钟可以跑到 40MHz 甚至更高但前提是接线要短、PCB 走线要等长。我一开始用杜邦线飞线时钟上到 20MHz 就开始花屏后来改成 PCB 走线40MHz 稳如老狗。所以如果你打算长期用建议直接画板子别省这一步。2.3 传感器与电源心率、加速度计、电池管理手表要有点“智能”的样子传感器少不了。我选了三个LIS3DH 三轴加速度计做计步和抬腕检测MAX30102 做心率和血氧TP4056 做锂电池充电管理。LIS3DH 是 ST 自家的加速度计I2C 接口功耗低到 2μA低功耗模式支持中断输出。计步算法我放在 MCU 里跑通过读取三轴加速度的合矢量变化来判断步伐。这里有个坑LIS3DH 的中断引脚配置比较绕需要设置阈值、持续时间、滤波参数一开始我没调好导致轻微晃动就触发中断功耗飙升。后来把阈值调到 0.3g、持续时间设为 20ms才稳定下来。MAX30102 是心率和血氧传感器I2C 接口内置 FIFO。它的难点在于光学设计和算法。LED 驱动电流、采样率、FIFO 读取时机都会影响数据质量。我的做法是用 100Hz 采样率、LED 电流 7mA 左右FIFO 半满中断读取然后在 MCU 里做简单的峰值检测算心率。精度肯定比不上专业医疗设备但日常看看趋势够用了。电源部分TP4056 负责充电DW01 加 8205A 做保护ADC 通过分压电阻读电池电压。这里要注意分压电阻的阻值选择太大则 ADC 输入阻抗不匹配读数不准太小则静态功耗高。我最后用了 100k 和 100k 分压并联 0.1μF 电容滤波读数稳定性可以接受。3. 软件架构从裸机到状态机3.1 裸机还是 RTOS我的选择与理由软件架构是第二个岔路口。裸机写手表完全可行但代码组织不好会变成一团乱麻。RTOS 能简化任务调度但会增加 RAM 开销和上下文切换功耗。我最终选了裸机加状态机的方案理由有三一是手表的功能相对固定任务不多裸机足够二是 RTOS 的 tick 中断会频繁唤醒 CPU对低功耗不友好三是状态机让代码逻辑清晰每个状态做什么一目了然。我的主循环结构是这样的上电初始化后进入主循环主循环里先处理事件标志然后根据当前状态执行对应逻辑最后进 Stop 模式等中断唤醒。事件标志包括RTC 秒中断、按键中断、加速度计中断、充电状态变化中断。每个中断只做最少的处理置个标志就退出具体逻辑在主循环里跑。这种“中断置标志、主循环处理”的模式好处是中断响应快、主循环逻辑集中、功耗可控。坏处是如果某个事件处理时间过长会影响其他事件的响应。所以我在主循环里对耗时操作做了分片比如屏幕刷新一帧拆成多次每次刷一部分刷完再进 Stop。3.2 状态机设计待机、亮屏、菜单、充电四个状态手表的状态不多我定义了四个待机STANDBY、亮屏ACTIVE、菜单MENU、充电CHARGING。状态之间的切换由事件驱动。待机状态下屏幕关闭MCU 进 Stop 2只有 RTC 和 EXTI 在工作。RTC 每秒唤醒一次更新内部时间变量然后立刻回 Stop。按键中断或抬腕中断触发后切到亮屏状态。亮屏状态下屏幕打开显示表盘MCU 保持运行。如果 5 秒内没有操作自动回待机。如果有按键长按切到菜单状态。菜单状态下屏幕显示菜单项按键上下切换短按确认长按返回。菜单里可以设置时间、查看计步、启动心率测量等。充电状态下屏幕显示充电图标和电量百分比MCU 保持运行但关闭传感器减少发热。状态切换的代码我用 switch-case 实现每个状态一个函数函数里先判断是否有状态切换事件有就返回新状态没有就执行当前状态的逻辑。这样主循环里只需要调用当前状态函数根据返回值决定是否切换。3.3 时间管理RTC 走时精度与校准RTC 是手表的核心走时不准一切白搭。STM32L4 的 RTC 用外部 32.768kHz 晶振时精度取决于晶振本身的误差和负载电容的匹配。我一开始用的是普通的 32.768kHz 圆柱晶振日误差在几秒到十几秒之间。后来换了精度更高的晶振±5ppm并仔细调整了负载电容日误差降到了 1 秒以内。这里有个细节STM32 的 RTC 支持数字校准可以通过 RTC_CALR 寄存器调整时钟频率。具体做法是测量一段时间内的误差算出 ppm 值然后写入校准寄存器。比如日误差 2 秒对应约 23ppm就在 CALR 里设置相应的校准值。这个功能很实用可以把普通晶振的精度提升一个档次。另外RTC 的备份域供电也要注意。VBAT 引脚接纽扣电池或超级电容主电源掉电时 RTC 继续走时。我用的是 0.1F 超级电容充满后可以维持 RTC 走时十几个小时日常使用足够了。4. 屏幕驱动与 UI 设计让圆屏显示表盘4.1 GC9A01 初始化那些手册上没写的细节GC9A01 的初始化序列比较长手册里给的是一套通用参数但实际用起来还需要根据屏幕厂商的差异做调整。我踩过的坑主要有两个一是颜色格式二是显示方向。颜色格式方面GC9A01 支持 RGB565 和 RGB666默认是 RGB565。但有些批次的屏幕出厂时 MADCTL 寄存器的 BGR 位没设对导致红色和蓝色互换。我一开始显示的表盘是蓝色的折腾了半天才发现是 BGR 位的问题。解决办法是在初始化序列里显式设置 MADCTL 寄存器把 BGR 位置 1 或清 0试一次就知道哪个对了。显示方向方面圆屏没有“横竖”之分但表盘的 12 点方向需要和实际佩戴方向一致。这涉及到 MADCTL 的 MX、MY、MV 三个位。我的做法是先把屏幕点亮显示一个不对称的图案比如一个箭头然后旋转屏幕看箭头指向调整 MADCTL 直到箭头朝上。初始化序列里还有几个关键参数帧率控制、电源控制、伽马校正。帧率我设的是 60Hz再高功耗上去了收益不大。伽马校正用默认值就行除非你对色彩有特别要求。4.2 表盘绘制用 DMA 刷屏降低 CPU 占用240x240 的 RGB565 一帧是 115200 字节如果全屏刷新SPI 按 40MHz 算传输时间约 23ms。这 23ms 里如果 CPU 全程参与功耗会很高。我的做法是用 DMA 把帧缓冲搬到 SPI 数据寄存器CPU 只负责准备数据传输交给 DMA传完之后 DMA 中断唤醒 CPU 准备下一帧。帧缓冲我开了两块一块用于当前显示一块用于绘制下一帧。绘制完成后切换指针DMA 从新缓冲区搬数据。这样绘制和传输可以重叠刷屏更流畅。表盘的绘制内容分三层背景层渐变或纯色、刻度层12 个刻度点和数字、指针层时针、分针、秒针。背景层和刻度层是静态的只在切换表盘样式时重绘指针层每秒钟重绘一次。为了减少重绘面积我只重绘指针经过的区域而不是全屏刷新。具体做法是记录上一秒指针的位置擦除旧指针绘制新指针两次绘制的区域合并后只刷这一块。这里有个实操技巧指针的绘制用画线算法Bresenham比用图形库的 drawLine 快很多。秒针我用的是 1 像素宽的线时针和分针用 3 像素宽的线视觉上更协调。4.3 低功耗刷新策略局部刷新与息屏手表的屏幕是功耗大户GC9A01 全亮时电流在 20mA 左右背光占大头。我的低功耗策略分三级全亮、半亮、息屏。全亮用于操作时背光 PWM 占空比 100%。半亮用于待机表盘占空比 30%既能看清时间又省电。息屏用于长时间无操作背光关闭屏幕进睡眠模式MCU 进 Stop 2。息屏后 RTC 每秒唤醒但屏幕不刷新只更新时间变量。按键或抬腕中断触发后先开背光再刷新屏幕然后切到亮屏状态。这样从息屏到亮屏的响应时间在 100ms 以内体感上基本是即时的。背光 PWM 我用 TIM3 的通道 1 输出频率 1kHz占空比可调。频率太低会有频闪太高则开关损耗大1kHz 是个折中。5. 功耗优化从毫安到微安的实战5.1 实测各模式电流数据说话功耗优化不能靠猜得实测。我用万用表和电流探头测了各个模式下的电流数据如下模式电流说明运行模式全速12mA80MHz屏幕全亮传感器开启运行模式降频4mA16MHz屏幕半亮传感器间歇Stop 2RTC 走时1.8μA屏幕关闭RAM2 保留Stop 2RTC加速度计中断3.5μA加速度计低功耗模式待机屏幕息屏MCU Stop 2约 25μA含屏幕睡眠电流和 LDO 静态电流从数据看MCU 本身的 Stop 2 电流很低但屏幕睡眠电流和 LDO 静态电流加起来有 20μA 左右。所以如果想进一步压功耗得从屏幕和电源芯片下手。我后来换了一款静态电流更低的 LDOTPS7A02静态电流 25nA整体待机电流降到了 8μA 左右。5.2 外设时钟门控不用就关STM32 的外设时钟默认是开启的不用的话要手动关。我在初始化完成后把不用的外设时钟全部关闭包括 ADC、DAC、USB、CAN 等。用到的外设也尽量在不用时关时钟比如心率传感器只在测量时开 I2C 时钟测完就关。GPIO 的处理也有讲究。未使用的 GPIO 配置为模拟输入避免浮空输入导致的漏电流。用到的 GPIO 根据场景配置输出引脚设推挽输出输入引脚设上拉或下拉中断引脚设浮空但开启中断。这里有个容易忽略的点调试接口。SWD 引脚在运行时如果保持调试模式会有额外功耗。我的做法是在正式固件里禁用 SWD需要调试时再通过特定按键组合重新使能。禁用 SWD 后待机电流能再降 1-2μA。5.3 传感器间歇工作心率与计步的功耗平衡心率和计步是两大耗电功能。心率传感器 MAX30102 工作时电流约 10mA计步用的 LIS3DH 低功耗模式约 2μA。所以策略很明确计步常开心率按需开启。计步方面LIS3DH 配置为 10Hz 采样率、低功耗模式通过中断唤醒 MCU 读取数据。10Hz 对于计步足够了再高就是浪费。中断阈值设为 0.3g持续时间 20ms这样正常走路能触发轻微晃动不触发。心率方面用户主动启动测量时才开 MAX30102测 30 秒后自动关闭。测量期间屏幕显示心率曲线测完保存结果。如果用户不主动测心率传感器全程关闭不耗电。抬腕检测也用 LIS3DH但用的是另一个中断源阈值设得比计步高一些0.5g避免误触发。抬腕后亮屏 5 秒无操作自动息屏。6. 调试与踩坑那些让我熬夜的瞬间6.1 屏幕花屏从杜邦线到 PCB 的教训花屏是我遇到的第一个大坑。现象是屏幕显示正常但偶尔出现彩色条纹或局部闪烁。一开始我以为是驱动问题换了几个驱动库都不行。后来用示波器看 SPI 时钟和数据线发现时钟上升沿有振铃数据线在时钟边沿附近有毛刺。原因很明确杜邦线太长信号完整性差。杜邦线没有阻抗控制线间电容大40MHz 的 SPI 时钟在杜邦线上传输会严重失真。我把杜邦线剪短到 5cm 以内花屏频率明显降低但还是偶尔出现。最后画了 PCBSPI 走线等长、包地问题彻底消失。这个坑的教训是高速信号一定要用 PCB杜邦线只适合低速调试。如果你非要用杜邦线把时钟降到 10MHz 以下能稳定不少。6.2 RTC 走时不准晶振负载电容的匹配RTC 走时不准是第二个大坑。现象是每天慢十几秒换了几个晶振都一样。我查了 STM32 的 RTC 章节发现负载电容的匹配很关键。32.768kHz 晶振的负载电容通常是 6pF 或 12.5pF而 STM32 的 RTC 振荡器内部有可调的负载电容通过 RTC_CALR 寄存器配置。我的做法是先用示波器测晶振引脚的正弦波看频率是否准确。如果不准调整内部负载电容值每次调一档测一天误差直到误差最小。这个过程比较耗时但效果显著。调整后日误差从十几秒降到了 1 秒以内。另外晶振的布局也很重要。晶振要尽量靠近 MCU 的 OSC32 引脚走线短且包地远离高频信号线。负载电容要选温度系数好的 C0G 材质避免温度变化导致频率漂移。6.3 低功耗模式唤醒失败中断优先级的坑进 Stop 模式后唤醒失败是我遇到的第三个大坑。现象是 MCU 进了 Stop 2 之后按键中断无法唤醒必须复位才能恢复。我查了参考手册发现 Stop 2 模式下只有特定的中断能唤醒而且中断优先级配置有要求。具体来说EXTI 中断要配置为唤醒源需要在 EXTI_IMR 寄存器里使能对应的中断线并且在 NVIC 里使能对应的中断通道。另外RTC 闹钟中断也要使能。我一开始只使能了 EXTI忘了 RTC 闹钟导致每秒唤醒失败MCU 一直停在 Stop 2 里出不来。还有一个细节进 Stop 模式前要清除所有挂起的中断标志否则一进 Stop 就立刻被挂起的中断唤醒等于没进。我的做法是在进 Stop 前调用__HAL_RCC_CLEAR_RESET_FLAGS()和__HAL_PWR_CLEAR_FLAG()把相关标志清干净。6.4 电池电量显示跳变ADC 采样与滤波电池电量显示跳变是第四个坑。现象是电量百分比在短时间内大幅波动比如从 80% 跳到 60% 又跳回 75%。原因是 ADC 采样值受负载影响屏幕亮和屏幕暗时电池电压不同导致读数波动。解决办法有两个一是硬件上加 RC 滤波在分压电阻两端并联 0.1μF 电容二是软件上做滑动平均滤波连续采样 16 次取平均并且只在屏幕状态稳定时采样。我两个都做了电量显示稳定了很多波动在 2% 以内。另外锂电池的电压和电量不是线性关系3.7V 到 4.2V 之间电量变化快3.4V 到 3.7V 之间变化慢。所以电量百分比不能简单按电压线性映射得用查表法或者分段线性拟合。我用的是分段线性拟合把 3.4V-4.2V 分成 8 段每段对应一个电量区间显示效果比较符合直觉。7. 外壳与佩戴从裸板到能戴出门7.1 3D 打印外壳尺寸测量与公差处理裸板调通之后下一步是让它能戴出门。我用的方案是 3D 打印外壳材料是 PLA打印精度 0.1mm。外壳分三部分表壳、表盖、表带接口。表壳的设计要点是内腔尺寸要比 PCB 大 0.3mm留出装配公差屏幕开窗要比屏幕可视区大 0.2mm避免遮挡按键开孔要对准按键位置孔径比按键帽大 0.1mm。这些公差看着小但差一点就装不进去或者晃动。表盖的设计要点是卡扣要能卡住表壳但不能太紧否则拆不下来屏幕保护用亚克力片厚度 1mm用双面胶固定在表盖上。表带接口用标准的 20mm 生耳方便换表带。打印参数方面层高 0.1mm填充 20%外壳壁厚 1.2mm。PLA 的强度够用但耐温性差夏天户外暴晒可能会变形。如果介意可以换 PETG 或 ABS。7.2 电池与充电200mAh 够不够用电池我选的是 200mAh 的锂聚合物电池尺寸 30x20x4mm刚好塞进表壳。续航实测待机息屏、计步开启约 5 天正常使用每天亮屏 1 小时、测心率 3 次约 3 天重度使用频繁操作、常亮屏幕约 1 天。这个续航水平不算惊艳但比一天一充的智能手表强不少。充电用的是 TP4056 模块Micro USB 接口。充电电流设的是 500mA充满 200mAh 电池约 30 分钟。充电时屏幕显示充电图标和百分比充满后自动停充。这里有个细节TP4056 的充电状态引脚可以接到 MCU 的 GPIO用来检测充电状态。我把它接到 EXTI 中断充电插入和拔出时唤醒 MCU切换充电状态。7.3 佩戴体验重量、厚度与表带选择整机重量约 45g厚度 12mm戴在手上不算轻但可以接受。表带我用的是一般的硅胶表带20mm 宽柔软度还行。如果你追求轻量化可以换尼龙表带能轻 10g 左右。佩戴体验上最大的问题是屏幕在户外强光下看不清。GC9A01 的亮度最高约 300cd/m²阳光下勉强能看但对比度差。解决办法是提高背光亮度但功耗上去了。我的折中是户外模式下手动调高亮度室内自动降低。另外手表的背面要贴皮肤心率传感器的光学窗口要贴合皮肤才能测准。我在表壳背面开了个窗口用透明亚克力片密封传感器紧贴窗口。实测心率数据在静止时比较准运动时因为晃动会有误差这是光学心率传感器的通病暂时无解。8. 后续可以怎么玩扩展思路与开源计划这块手表目前的功能是走时、计步、心率、充电管理、表盘切换。后续我想加的功能有几个方向。一是蓝牙同步。加一个 BLE 模块比如 nRF52832 或 ESP32-C3把手表数据同步到手机手机也能推送通知到手表。这个工作量不小涉及到 BLE 协议栈和手机 App 开发但可玩性很高。二是 GPS 定位。加一个低功耗 GPS 模块记录运动轨迹。不过 GPS 功耗高对手表续航是个挑战可能得做成可拆卸模块。三是更丰富的表盘。目前只有两种表盘样式我想做一个表盘编辑器用户可以在手机或电脑上设计表盘然后通过蓝牙或 USB 同步到手表。这个需要设计一套简单的表盘描述格式以及对应的渲染引擎。四是开源。我打算把硬件原理图、PCB、固件代码整理后开源放在代码托管平台上。开源的好处是大家可以一起改进也能让更多人少踩我踩过的坑。不过开源之前得把代码整理干净去掉那些临时调试的代码补上注释和文档。如果你也在做类似的项目或者对某个环节有疑问欢迎交流。STM32 做手表这件事技术难度不算特别高但涉及的细节很多每一个细节都可能成为坑。我的经验是先跑通最小系统再逐步加功能每加一个功能就测一次功耗和稳定性不要一次全加上去否则出了问题很难定位。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门