STM32手表低功耗与 OTA 设计

2026-11-141
AI 快速预览详细 收起
本文详细介绍了基于STM32F411系列的智能手表设计,重点讨论了低功耗管理和OTA升级的实现。硬件部分包括TPS63020电源管理、多种传感器(如MPU6050、EM7028、SPL06-001、LSM303DLHC)和蓝牙模块KT6368A。软件架构采用FreeRTOS和LVGL,支持抬腕亮屏、记录步数、测量心率等功能。通过RTC周期唤醒结合姿态判断来优化低功耗设计,确保用户体验的同时降低功耗。此外,还介绍了Bootloader和APP区域的分离,确保无线传输的安全性和可靠性。

手表在抬腕亮屏、记录步数、测量心率和无线升级之间不停切换,真正难处理的不是把器件堆在一块小 PCB 上,而是让每一条数据都在合适的时间流动。OV-Watch 把 Core 板和 Back 板叠在一起,以 STM32F411 系列为核心,外接显示、触摸、惯性、光学、气压、地磁和蓝牙模块,软件侧再用 FreeRTOS、LVGL、页面栈和 Bootloader 把这些功能组织成可维护的运行链路。下面沿着电源、传感器、任务和升级路径拆开看,重点放在硬件为什么这样接以及代码怎样把它们串起来。

OV-Watch 成品图,表盘和多功能界面集成在小尺寸外壳中

图 1 OV-Watch 成品外观

两块板把电源和传感器放进同一条链路

这块手表的 PCB 分为 Core 板和 Back 板。Core 板承载主控、显示和主要接口,Back 板集中放置传感器与蓝牙模块,两个板子贴合后才能在有限面积内同时留下电池、屏幕和佩戴空间。项目说明里出现过 STM32F411CUE6 和 STM32F411CEU6 两种写法,封装选型不能只按文字照抄,应以工程中的器件封装和原理图为准。

电源从 3.7 V 锂电池进入 TPS63020,再得到 3V3 供给 MCU、传感器和逻辑部分。V2.2 以后把充电触点改为磁吸焊盘,避免无线充电线圈带来的体积和散热负担。蓝牙在 V2.4 换成 KT6368A,保留 SPP 通道后,升级数据可以与日常通信共用无线链路。显示侧使用 LCD 和 LVGL,触摸输入由 CST816 接收,按键则直接参与唤醒和菜单操作。

OV-Watch 裸板与屏幕实物,Core 板和显示组件可以看到排线与接口

图 2 裸板、屏幕和接口实物

硬件节点在系统中的作用与固件的对应关系
TPS63020把电池电压变成稳定的 3V3Power_Enable 控制总电源使能
STM32F411 系列运行 FreeRTOS、LVGL 和应用任务HAL、CMSIS-RTOS 和用户任务
MPU6050识别抬腕、放下和步数相关运动MPU_isHorizontal 与手势状态
EM7028提供心率采样数据,血氧功能仍取决于完整实现HR_Calculate 处理采样结果
SPL06-001读取气压并换算海拔气压任务按需唤醒
LSM303DLHC提供电子指南针数据指南针页面打开时唤醒
KT6368A蓝牙通信和无线升级BLE 初始化、启用和关闭
外部 EEPROM保存设置、日期和步数SettingGet 与 SettingSave

功耗测量还可以从代码反推。电池电压由 ADC 读取后累加 8 次再右移 3 位,相当于用简单平均降低抖动,充电时再减去一个内部电阻压降估计值。电量百分比不是电化学模型,而是按电压区间离散映射,因此换电芯或改变负载后需要重新校准区间。

for (i = 0; i < 8; i++)
{
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 5);
    dat += HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
    delay_ms(1);
}
dat = dat >> 3;
BatVoltage = dat * 2 * 3.3 / 4096;

这段实现把 ADC 满量程、前端分压比和参考电压直接写进公式。它适合快速得到电量显示,但如果要提升一致性,还要把实际参考电压、分压误差和电池放电曲线纳入标定。

传感器数据怎样在界面上汇合

软件架构从底向上分成硬件驱动、BSP、HAL、CMSIS-OS 与 FreeRTOS、中间件 LVGL,最上层是任务和页面。这样分层的好处是同一个传感器可以在真实硬件和 Windows 仿真工程里复用上层页面,硬件访问则集中在 HWDataAccess 一类接口中。软件架构图里把 UI、任务和 PageManager 放在应用层,正好对应了这种依赖方向。

OV-Watch 软件架构图,展示硬件、驱动、中间件和应用层的关系

图 3 软件架构与任务分层

OV-Watch 软硬件数据流重绘图,展示电源、传感器、任务和页面的关系

图 4 软硬件数据流重绘,箭头表示数据或控制关系

传感器并不是一直全速运行。心率页面打开时,任务先唤醒 EM7028,再对采样结果调用 HR_Calculate,算出合理范围内的心率后才更新共享状态。指南针页面则先唤醒 LSM303DLHC,读取完成后再让它回到休眠。这样的按页启停把传感器功耗和用户行为绑在一起,也避免了所有数据都塞进主循环。

抬腕判断是低功耗链路中的特殊一环。 MPU6050 负责提供姿态信息,任务每隔 300 ms 检查一次水平状态。检测到从放下变为抬起时,停止模式任务才会继续恢复屏幕和串口;如果重新检测到放下,就再次回到睡眠队列。关键逻辑可以压缩成下面这样。

if (MPU_isHorizontal())
{
    HWInterface.IMU.wrist_state = WRIST_UP;
}
else if (WRIST_UP == HWInterface.IMU.wrist_state)
{
    HWInterface.IMU.wrist_state = WRIST_DOWN;
    osMessageQueuePut(Stop_MessageQueue, &Stopstr, 0, 1);
}

这里的队列消息只表示状态变化,不携带整包传感器数据。传感器任务负责判断,运行模式任务负责切换电源和显示,两个职责分开后,抬腕逻辑不会直接修改 LCD 或时钟配置。

睡眠模式把唤醒条件留给 RTC 和动作判断

系统实际有运行、睡眠和关机三个层次。运行模式下 MCU、屏幕和蓝牙都可工作,睡眠模式让 MCU 进入 STOP,MPU6050 仍用于记步或姿态判断,关机模式则关闭 TPS63020 的使能,只留下电池侧和 RTC 所需的电源。项目说明里给出的典型电流是运行模式 70 到 80 mA,待机约 1 mA,关机只保留 RTC 的极小电流,这些数字应被当作特定硬件和测试条件下的参考。

进入 STOP 之前要先处理会阻止低功耗的外设。代码先撤销串口 GPIO,关闭 LCD 背光并让触摸控制器休眠,再暂停调度器和 SysTick 中断 ,最后执行 STOP 模式。唤醒后重新配置系统时钟、恢复 SysTick 和看门狗,重新初始化串口、LCD 和触摸,再把首页更新消息放回队列。

HAL_UART_MspDeInit(&huart1);
LCD_RES_Clr();
LCD_Close_Light();
CST816_Sleep();

vTaskSuspendAll();
CLEAR_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);
HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI);

SET_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);
HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq));
SystemClock_Config();
xTaskResumeAll();

早期设计曾尝试直接用 MPU6050 的运动中断唤醒,但需要较大的抖动幅度才容易触发,后来改成 RTC 周期唤醒后再检查姿态。这个取舍说明低功耗不只是把 MCU 设成 STOP,还要在唤醒频率、动作阈值和用户体验之间平衡。若把 MPU6050 的 DMP 初始化原样保留,待机电流也可能偏高,所以传感器初始化和休眠配置必须一起审查。

Bootloader 把无线升级的风险挡在 Flag 之后

V2.4 之后固件拆成 Bootloader 和 APP 两个区域。Boot 区后面留出 Flag 区,只有 APP 数据传输完成并确认完整后才写入 APP FLAG。上电时 Bootloader 先检查按键,如果按住 KEY1 就进入升级菜单,否则读取 Flag,确认通过后才跳转到 APP。这样即使 无线传输 中途断电,也不会把一个未完成的镜像当成可启动程序。

Bootloader 与 APP 的启动判断流程,Flag 区决定是否跳转

图 5 Bootloader 启动判断流程

代码中的检查是把 Flag 区的两个 32 位数据拼成字符串,再与 APP FLAG 比较。跳转前关闭 SysTick 和全局中断,读取 APP 向量表中的栈顶与复位地址,设置主堆栈后进入应用。

uint32_t address = 0x08008000;
data1 = *(uint32_t *)address;
data2 = *(uint32_t *)(address + sizeof(uint32_t));

memcpy(str1, &data1, sizeof(data1));
memcpy(str2, &data2, sizeof(data2));
str1[4] = '\0';
str2[4] = '\0';
strcpy(combined_str, str1);
strcat(combined_str, str2);

if (strcmp(combined_str, "APP FLAG") == 0)
{
    SysTick->CTRL = 0X00;
    __disable_irq();
    JumpAddress = *(__IO uint32_t *)(APPLICATION_ADDRESS + 4);
    Jump_To_Application = (pFunction)JumpAddress;
    __set_MSP(*(__IO uint32_t *)APPLICATION_ADDRESS);
    Jump_To_Application();
}

这个方案的关键不在字符串本身,而在 Flag 写入时机。升级任务需要先擦写和校验 APP 区,再写入 Flag,任何一步失败都应让 Bootloader 停在下载菜单。实际移植时还要核对 APPLICATION_ADDRESS、向量表偏移和链接脚本,Bootloader 与 APP 的中断入口必须保持一致。

FreeRTOS 和 LVGL 把任务节奏与页面刷新拆开

应用入口先初始化 HAL 外设,再调用 osKernelInitialize、MX_FREERTOS_Init 和 osKernelStart。硬件初始化任务负责传感器、EEPROM、蓝牙、触摸和 LCD,传感器任务按页面需要更新数据,按键、充电、页面刷新和数据保存各自通过队列或事件标志协作。LVGL 只负责控件和绘制,真正的硬件访问由驱动层完成,避免页面代码直接操作 GPIO。

页面切换使用栈来保存返回路径。加载新页面前先反初始化当前页面,再把页面对象压入栈,初始化新页面并交给 LVGL 做切换动画。这样从设置页进入传感器页后,返回键可以准确回到上一个页面,不需要为每一对页面写专门的跳转分支。

if (PageStack.top > 0)
{
    PageStack.pages[PageStack.top - 1]->deinit();
}
page_stack_push(&PageStack, newPage);
newPage->init();
lv_scr_load_anim(*newPage->page_obj,
                LV_SCR_LOAD_ANIM_MOVE_RIGHT, 100, 0, true);

页面栈保存的是 Page_t 指针,而不是页面结构体的临时副本。指针生命周期和页面初始化顺序必须保持稳定,否则返回时会访问已经变化的对象。这个细节看似只属于 UI,却直接影响 FreeRTOS 任务向哪个页面投递数据。

设置和步数数据放在外部 EEPROM 中。初始化时先用 0x55 和 0xAA 做存储器自检,再从 0x10 一带读取抬腕开关等设置,从 0x20 一带读取日期和步数。读出的值还需要检查范围,空白 EEPROM 的 0xFF 不能直接当作有效配置。

OV-Watch 佩戴实物,屏幕、触摸和抬腕交互在同一设备上完成

图 6 佩戴状态与主界面

把电源使能、传感器休眠、任务队列、页面栈和升级 Flag 放在一条链路里看,才能解释这块手表为什么既能保持多功能界面,又能在静置时把功耗压下来。硬件上每增加一个传感器,软件就要多承担一次初始化、唤醒、异常和数据边界处理;只有这些边界都被代码明确表达,后续更换蓝牙模块、增加页面或扩展传感器时,系统才不会因为一个局部改动破坏整机运行。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -