1. 为什么我要用STM32做一块手表
1.1 从“买不起”到“做一块”的动机
市面上能看消息、能测心率、能换表盘的智能手表,价格从几百到几千不等。但如果你是一个嵌入式开发者,或者正在学STM32,心里多少会有个念头:这东西我自己能不能做一块?我第一次冒出这个想法,是因为看到学生用STM32做毕业设计,做的是一个能显示时间、能计步、能测温度的手表。当时觉得,这不就是把几个模块拼在一起吗?后来自己真正动手才发现,拼起来容易,拼好很难。
STM32手表这个项目,核心就是用一颗STM32微控制器,驱动一块小尺寸LCD或OLED屏幕,配合实时时钟芯片、加速度传感器、电池管理电路,做成一个可以戴在手腕上的设备。它解决的不是“刚需”问题,而是让你把GPIO、I2C、SPI、定时器、中断、低功耗模式、OTA升级这些知识点,在一个看得见摸得着的项目里全部串起来。适合谁?适合已经学过STM32基础外设、想找一个综合性项目练手的人,也适合正在找毕业设计题目的同学。
1.2 这个项目到底包含哪些技术点
一块STM32手表,麻雀虽小,五脏俱全。它至少涉及以下内容:
- 主控选型:STM32F103、STM32F411、STM32L4系列都有人用,选型决定了功耗、性能和开发难度。
- 显示驱动:SPI接口的OLED或LCD,需要写驱动、做UI刷新。
- 实时时钟:内部RTC或外置DS3231,前者便宜但精度差,后者贵但省心。
- 电源管理:锂电池充放电、LDO或DC-DC降压、电量检测。
- 传感器:加速度计做计步和抬腕亮屏,心率传感器做健康监测。
- 低功耗设计:待机电流决定续航,是手表能不能戴出门的关键。
- 固件升级:OTA功能让手表不用拆机就能更新程序。
这些技术点单独拿出来都不难,但放在一起,就会遇到资源冲突、时序打架、功耗超标等各种问题。我踩过的坑,后面会一个个说。
1.3 适合什么样的读者
如果你刚学完STM32的GPIO和串口,还没做过完整项目,这篇文章会带你走一遍从选型到调试的全过程。如果你已经做过一些模块,但没做过低功耗设备,那电源管理和功耗优化这部分会对你有用。如果你只是想找个毕业设计题目,这里的技术路线和实现细节可以直接参考。我不打算写成教科书,而是按照我实际做这块表的顺序,把关键决策和踩坑经验讲清楚。
2. 硬件选型与核心电路设计
2.1 主控芯片怎么选:F103还是L4
选主控是第一个岔路口。STM32F103C8T6是最常见的选择,价格便宜,资料多,社区里随便一搜就有大量例程。但它的问题也很明显:功耗偏高,运行模式下的电流在几十毫安级别,待机模式虽然能降到微安级,但唤醒后重新初始化外设的时间较长。如果你做的是插着充电宝玩的手表,F103完全够用。但如果你想让它像真正的手表一样,充一次电用几天,那就要考虑STM32L4系列。
STM32L476或L452是低功耗系列的代表,运行模式下电流可以做到100μA/MHz左右,Stop模式下不到1μA。代价是价格比F103贵一些,而且低功耗模式下的外设配置更复杂,比如唤醒后时钟需要重新配置,有些外设需要重新初始化。我第一版用的是F103,因为手头正好有最小系统板。第二版换成了L452,续航从半天提升到了四天。如果你刚开始做,建议先用F103把功能跑通,再考虑换低功耗芯片。
2.2 屏幕选型:OLED还是LCD
手表屏幕一般不会超过1.3英寸。常见的选择有两种:SPI接口的OLED和SPI接口的TFT LCD。OLED的优点是自发光、对比度高、视角广,显示黑色时几乎不耗电,适合做深色表盘。缺点是尺寸小、分辨率低,常见的是0.96寸128x64或1.3寸128x64。TFT LCD的优点是分辨率高、色彩丰富,可以做更复杂的UI,但需要背光,功耗更高。
我最终选了1.3寸的SPI OLED,驱动芯片是SH1106或SSD1306。这两个芯片的驱动方式类似,但SH1106的显存是132x64,实际屏幕是128x64,需要做偏移处理。如果你买的是SSD1306,直接按128x64写就行。屏幕的刷新率不用太高,手表界面一般只有秒针在动,每秒刷新一次就够。但如果你要做动画,比如抬腕亮屏的过渡效果,那就要把SPI速率提上去,或者用DMA传输。
2.3 实时时钟:内部RTC还是外置DS3231
STM32内部有RTC,配合32.768kHz晶振可以走时。但内部RTC的精度受晶振和温度影响很大,一天误差几秒到几十秒都有可能。如果你只是做个demo,内部RTC够用。但如果你想让手表一个月误差不超过一分钟,那就需要外置DS3231。DS3231内置温补晶振,精度可以做到±2ppm,也就是一年误差一分钟左右。
DS3231通过I2C接口和STM32通信,电路上需要接上拉电阻,一般4.7kΩ。它还有一个闹钟输出引脚,可以接到STM32的外部中断,用来做闹钟唤醒。我用的是DS3231模块,直接买现成的,省去了画电路和焊接的麻烦。如果你要自己画板,注意DS3231的电池座要选好,CR2032电池座的高度会影响手表厚度。
2.4 电源管理:锂电池充放电与稳压
手表用的小锂电池一般是3.7V的软包电池,容量在100mAh到300mAh之间。充电管理芯片常用TP4056,它负责把USB的5V降到4.2V给电池充电,同时有过充过放保护。但TP4056的输出是电池电压,而STM32和屏幕需要3.3V,所以还需要一个LDO或DC-DC降压。
LDO的优点是电路简单、噪声低,缺点是效率低,电池电压降到3.3V时,多余的能量全部变成热量。DC-DC的效率高,但电路复杂,需要电感,而且开关噪声可能影响RTC和传感器。我第一版用了LDO,型号是XC6206,静态电流很小,适合低功耗场景。第二版换成了TPS62740,这是一款超低静态电流的DC-DC,效率能到90%以上。如果你对手表厚度有要求,DC-DC的电感会占地方,需要权衡。
2.5 传感器选型:加速度计与心率传感器
加速度计用来做计步和抬腕亮屏。常见的选择是LIS3DH或MPU6050。LIS3DH是三轴加速度计,功耗低,有内置的FIFO和中断功能,适合做低功耗计步。MPU6050是六轴,带陀螺仪,但功耗更高,而且DMP库用起来比较麻烦。我选了LIS3DH,通过I2C接口读取数据,配置好阈值中断后,STM32可以在Stop模式下等待加速度计的中断唤醒,不用一直轮询。
心率传感器我用的比较少,因为手腕上的心率测量受佩戴位置和运动影响很大。常见的是MAX30102,通过I2C读取红光和红外光的反射数据,用算法计算心率。这个传感器对算法要求较高,简单的峰值检测在运动时基本不可用。如果你只是想在手表上显示一个心率数值,可以用现成的算法库,但精度不要期望太高。
3. 软件开发环境与工程搭建
3.1 Keil、STM32CubeMX与VSCode怎么配合
开发STM32,Keil MDK是最常见的选择,尤其是Keil5,可以兼容C51和STM32的芯片包。安装Keil5之后,需要单独安装STM32的Device Family Pack,也就是芯片包。如果你用的是STM32F1系列,就装F1的包;如果是L4系列,就装L4的包。芯片包安装失败是常见问题,一般是网络原因,可以手动下载pack文件,双击安装。
STM32CubeMX用来做引脚配置和时钟树初始化,生成HAL库代码。它的好处是直观,点一点就能把外设配好,时钟树也会自动计算分频和倍频。但生成的代码比较臃肿,如果你用标准库,就不需要CubeMX。标准库和HAL库的区别,简单说就是标准库更接近寄存器,代码效率高,但移植性差;HAL库抽象程度高,跨系列移植方便,但代码量大。我建议新手先用CubeMX+HAL库把功能跑通,等熟悉了再考虑标准库或LL库。
VSCode可以用来写代码,配合Keil Assistant插件或者STM32CubeIDE,可以实现代码补全和跳转。但调试还是用Keil或STM32CubeIDE更方便,因为ST-Link Utility和调试器集成得更好。我现在的流程是:CubeMX生成工程,VSCode写代码,Keil编译和下载。如果你习惯用VSCode,可以装STM32的插件,但配置起来需要一些时间。
3.2 新建工程的几个关键步骤
新建一个STM32工程,不管用哪种方式,核心步骤都差不多:
- 选芯片型号:在CubeMX里选具体的型号,比如STM32F103C8T6。
- 配置时钟源:选择外部晶振HSE,然后在时钟树里把系统时钟拉到最高,F103是72MHz,L4是80MHz。
- 配置调试接口:在SYS里把Debug改成Serial Wire,否则下载一次后SWD引脚可能被禁用,导致无法再次下载。
- 配置外设:按需开启I2C、SPI、USART、定时器等。
- 配置中断优先级:RTC、加速度计、串口的中断优先级要合理安排,避免高优先级中断阻塞低优先级。
- 生成代码:选择HAL库或LL库,生成Keil工程。
这里有一个坑:如果你在CubeMX里禁用了JTAG但保留了SWD,下载和调试都正常。但如果你不小心把SWD也禁用了,那就只能用串口下载或者用STM32CubeProgrammer通过BOOT引脚进入系统存储器来擦除。我踩过这个坑,后来每次都在SYS里确认Debug是Serial Wire。
3.3 串口调试与printf重定向
串口是调试手表最重要的工具。你可以通过串口打印传感器数据、RTC时间、电量信息,甚至用串口发送命令来切换表盘。printf重定向是基本操作,在Keil里需要勾选Use MicroLIB,然后重写fputc函数。如果你用GCC,需要实现_write函数。
串口波特率一般用115200,数据位8,停止位1,无校验。如果你用USB虚拟串口,STM32F103不支持USB,需要F4或L4系列。USB虚拟串口的优点是速度快,而且不需要额外的USB转TTL模块,但配置起来比普通串口复杂,需要配置USB时钟和中断。
3.4 代码版本管理与OTA升级的准备
手表项目代码量不大,但版本多了容易乱。我建议用Git做版本管理,每次改完一个功能就提交一次。OTA升级需要提前规划Flash分区,一般把Flash分成Bootloader区和Application区。Bootloader负责接收新固件并写入Application区,Application负责正常运行。STM32F103的Flash是64KB或128KB,分区时要算好大小。如果Application超过分区大小,OTA就会失败。
OTA的通信方式可以用串口、蓝牙或WiFi。串口OTA最简单,但需要有线连接。蓝牙OTA需要额外的蓝牙模块,比如HC-05或JDY-31。WiFi OTA需要ESP8266或ESP32,通过AT指令或SPI接口和STM32通信。我第一版用的是串口OTA,第二版加了蓝牙,用手机App发送固件文件。OTA的难点在于固件校验和断点续传,如果传输过程中断电,要有机制恢复。
4. 核心功能实现与调试实录
4.1 屏幕驱动与UI刷新
屏幕驱动是手表的脸面。SPI OLED的驱动一般分两层:底层是SPI读写和命令/数据切换,上层是显存管理和字符/图形绘制。SSD1306的显存是128x64位,也就是1024字节。你可以定义一个全局数组作为显存,所有绘制操作先写到显存,再一次性刷新到屏幕。这样做的目的是避免频繁SPI传输,减少屏幕闪烁。
刷新方式有两种:全屏刷新和局部刷新。全屏刷新简单,但每次要传1024字节,SPI速率低的时候会看到明显的刷屏。局部刷新只更新变化的区域,但需要计算区域坐标和显存偏移。我一开始用全屏刷新,后来改成局部刷新,秒针跳动时只刷新秒针区域,视觉上流畅很多。
UI设计上,手表界面一般包含时间、日期、电量、步数。你可以用取模软件生成中文字库,但字库会占用大量Flash。如果Flash紧张,可以只保留常用汉字,或者用英文和数字。我用的方案是时间用大号数字,日期和电量用小号英文,步数用图标加数字。
4.2 RTC走时与闹钟功能
RTC的初始化包括配置时钟源、设置分频、设置初始时间。STM32的内部RTC时钟源一般选LSE,也就是外部32.768kHz晶振。LSE的起振时间比较长,有时候需要几百毫秒。如果你发现RTC不走,先检查晶振是否起振,可以用示波器看波形,或者用CubeMX生成的代码读RTC标志位。
闹钟功能用RTC的Alarm中断实现。设置闹钟时间后,RTC会在匹配时产生中断,STM32从Stop模式唤醒,触发振动或蜂鸣器。这里要注意,RTC闹钟中断的清除方式比较特殊,需要先读RTC_ALRMAR寄存器,再写RTC_ISR的ALRAF位。如果清除顺序不对,中断会一直触发。
DS3231的闹钟功能更简单,它有两个闹钟寄存器,可以设置到秒、分、时、日。DS3231的INT引脚接到STM32的外部中断,配置成下降沿触发。DS3231的优点是闹钟精度高,而且不依赖STM32的RTC。
4.3 计步算法与抬腕亮屏
计步算法的核心是读取加速度计的三轴数据,计算合加速度,然后做峰值检测。合加速度的公式是sqrt(x²+y²+z²)。静止时合加速度约等于1g,走路时会在1g上下波动。你可以设置一个阈值,比如1.2g,当合加速度超过阈值时认为走了一步。但这样容易误判,比如手抖也会触发。
更稳定的做法是加一个时间窗口,比如两次步数之间至少间隔200ms,同时检测波峰和波谷。LIS3DH有内置的中断功能,可以配置成运动检测,当加速度超过阈值时产生中断,STM32在中断里计步。这样STM32可以大部分时间处于Stop模式,只在有运动时唤醒。
抬腕亮屏也是用加速度计实现。当手腕从下垂转到抬起时,加速度计的Y轴或Z轴会有明显变化。你可以设置一个角度阈值,当变化超过阈值时点亮屏幕。这个功能的难点是误触发,比如走路时手臂摆动也会触发。我的做法是结合陀螺仪数据,但LIS3DH没有陀螺仪,所以只能调阈值和延时。
4.4 低功耗模式与续航优化
低功耗是手表能不能戴出门的关键。STM32的低功耗模式有三种:Sleep、Stop、Standby。Sleep模式下内核停止,外设还在运行,唤醒最快,但功耗最高。Stop模式下所有时钟停止,保留寄存器和SRAM,唤醒后需要重新配置时钟。Standby模式下功耗最低,但SRAM丢失,相当于复位。
手表一般用Stop模式。在Stop模式下,RTC、LSE、IWDG可以继续运行,加速度计和DS3231的闹钟中断可以唤醒。唤醒后,系统时钟需要重新配置,外设需要重新初始化。我一开始没重新初始化SPI,结果屏幕不亮,后来在唤醒后加了SPI初始化才正常。
续航优化还包括关闭不用的外设、降低屏幕亮度、减少刷新频率。屏幕是最大的耗电大户,OLED显示黑色时几乎不耗电,所以深色表盘比浅色表盘省电。如果你用LCD,背光是主要耗电,可以考虑自动亮度或定时关闭背光。
4.5 OTA升级的实现与注意事项
OTA升级的流程是:Bootloader启动后检查是否有新固件标志,如果有,则接收固件并写入Application区,然后跳转到Application。如果没有,直接跳转到Application。Application在收到升级命令后,设置标志位,然后复位。
固件传输可以用串口或蓝牙。串口传输简单,但速度慢,115200波特率下,100KB的固件需要约10秒。蓝牙传输速度更慢,但方便。传输协议可以用YModem或自定义协议。YModem有现成的实现,支持断点续传和CRC校验。自定义协议需要自己处理分包、校验和重传。
OTA的坑很多。首先是Flash分区,Bootloader和Application的起始地址要算好,跳转前要关闭所有中断,设置栈指针。其次是固件校验,如果固件损坏,跳转后会死机。我建议在Bootloader里加CRC校验,校验通过才跳转。最后是断电保护,如果传输过程中断电,下次上电后Bootloader要能检测到未完成的升级,重新接收。
5. 常见问题与排查技巧实录
5.1 下载与调试常见问题
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| ST-Link无法识别芯片 | SWD引脚被禁用 | 用STM32CubeProgrammer通过BOOT0进入系统存储器擦除 |
| 下载后程序不运行 | 时钟配置错误 | 检查HSE是否起振,时钟树是否配置正确 |
| 串口无输出 | 波特率不对或TX/RX接反 | 确认波特率和接线,用示波器看TX波形 |
| 程序跑飞 | 中断优先级冲突 | 检查中断优先级分组和抢占优先级 |
| 延时函数卡死 | SysTick中断被禁用 | 检查是否在中断里调用了HAL_Delay |
ST-Link Utility是常用的下载工具,但有时候会提示“无法识别USB设备”。这通常是驱动问题,可以重新安装ST-Link驱动,或者换一个USB口。如果用的是国产ST-Link,可能需要升级固件。我遇到过ST-Link固件版本太旧,无法下载L4系列,升级固件后解决。
5.2 屏幕显示异常排查
屏幕不亮、花屏、闪烁是常见问题。不亮先检查供电和SPI接线,用示波器看SPI时钟和数据线是否有波形。花屏一般是显存初始化不对,或者SPI速率太高。SSD1306的SPI速率最高可以到10MHz,但如果你用杜邦线连接,速率太高会导致数据错误,建议降到4MHz以下。
闪烁一般是刷新方式问题。如果你在中断里刷新屏幕,而中断频率太高,屏幕会闪。建议在主循环里刷新,或者用DMA传输。DMA传输不占用CPU,但需要配置DMA通道和SPI的DMA请求。
5.3 RTC走时不准与晶振问题
RTC走时不准,先看晶振。32.768kHz晶振的负载电容一般是6pF或12.5pF,选错会导致频率偏移。如果你用DS3231,走时不准的概率很低,除非电池没电。内部RTC的精度受温度影响,夏天和冬天误差会变化。如果你对精度要求高,可以用外部RTC,或者定期用网络时间校准。
晶振不起振是另一个常见问题。LSE的起振时间较长,有时候需要几百毫秒。如果一直不起振,检查晶振是否焊接良好,负载电容是否匹配。有些STM32芯片的LSE驱动能力较弱,可以尝试降低驱动强度或换晶振。
5.4 功耗超标与电池续航短
功耗超标先测电流。用万用表串联在电池和电路之间,测待机电流。如果待机电流在毫安级别,说明有外设没关。常见的是屏幕背光、传感器、串口。串口在Stop模式下如果引脚配置不对,会有漏电流。建议把不用的引脚配置成模拟输入或下拉。
电池续航短还可能是电池容量虚标。我买过标称200mAh的电池,实际只有100mAh。可以用电池测试仪测一下实际容量。另外,LDO的静态电流也要考虑,XC6206的静态电流约1μA,但有些LDO的静态电流在几十微安,会明显影响续航。
5.5 OTA升级失败与固件损坏
OTA失败最常见的原因是Flash分区不对。Bootloader和Application的起始地址要严格按照分区表设置,跳转前要检查栈指针是否在有效范围内。如果Application的起始地址不是0x08000000,需要在Keil的Target里修改ROM地址。
固件损坏一般是传输错误。建议在Bootloader里加CRC32校验,校验不通过则丢弃。如果传输过程中断电,可以在Flash里加一个标志位,记录升级状态,下次上电后根据标志位决定是否重新接收。
6. 项目扩展与个人经验分享
6.1 可以继续加什么功能
手表做完基础功能后,可以扩展的方向很多。比如加一个气压计,做海拔显示;加一个环境光传感器,做自动亮度;加一个蜂鸣器,做闹钟和提示音;加一个振动马达,做无声提醒。如果你用的是L4系列,还可以加LoRa模块,做远程数据传输。我见过有人用STM32手表做空气质量检测,配合PM2.5传感器,做成一个可穿戴的环境监测设备。
软件方面,可以加表盘切换、消息通知、蓝牙通话。表盘切换需要把多个表盘的显存数据存在Flash里,切换时读取并刷新。消息通知需要蓝牙模块和手机App配合,STM32接收手机发来的通知内容并显示。蓝牙通话需要更复杂的协议栈,一般用现成的蓝牙模块,STM32只负责AT指令控制。
6.2 我踩过的几个坑
第一个坑是SWD引脚禁用。我在CubeMX里把JTAG禁用了,但忘了保留SWD,结果下载一次后芯片就锁了。后来用STM32CubeProgrammer的“Full Chip Erase”才救回来。从那以后,我每次都在SYS里确认Debug是Serial Wire。
第二个坑是RTC闹钟中断清除顺序。我按照手册写了清除代码,但中断一直触发。后来发现要先读RTC_ALRMAR,再写RTC_ISR的ALRAF位,顺序反了就不行。这个细节在参考手册里有,但很容易忽略。
第三个坑是OTA跳转后死机。原因是跳转前没有关闭中断,也没有设置栈指针。后来在跳转函数里加了__disable_irq()和__set_MSP(),问题解决。
6.3 给新手的几点建议
如果你刚开始做STM32手表,建议先用最小系统板加模块的方式,把功能跑通再画板。画板的时候注意电源走线,锂电池的电流较大,走线要宽。屏幕的FPC排线要固定好,否则容易接触不良。外壳可以用3D打印,但要注意屏幕开孔和按键位置。
代码方面,建议模块化,每个外设一个文件,方便调试和移植。低功耗模式不要一开始就加,先把功能跑通,再逐步优化功耗。OTA也不要一开始就做,等基础功能稳定了再加。
最后分享一个小技巧:如果你用Keil编译时提示“无法识别USB设备”,可以试试把ST-Link插到USB 2.0口,有些USB 3.0口兼容性不好。另外,Keil的编译速度可以通过关闭Browse Information来提升,但会失去代码跳转功能,调试阶段建议开着。