智能手表系统架构设计与嵌入式开发实践

在智能穿戴设备日益普及的今天,一块小小的腕上屏幕背后,是硬件、操作系统、中间件和用户交互之间精密协作的结果。从清晨第一眼查看时间,到全天候监测心率步数,再到接收消息提醒——这些看似简单的功能,实则依赖于一套高度集成又精细调优的嵌入式系统。

我们以 黄山派开发板 为实验平台,基于RISC-V架构构建了一款智能手表原型。这块开发板虽小,却集成了丰富的外设资源:TFT显示屏、触摸控制器、多轴传感器、低功耗蓝牙模块……它不仅是教学工具,更是一个理想的可穿戴设备研发沙盒。🎯

但问题来了:如何在一个主频仅400MHz、SRAM不超过512KB的MCU上,跑出流畅稳定的GUI界面?如何让多个传感器并发工作而不互相干扰?又该如何在保证响应速度的同时,把待机电流压到1.2mA以下?

这正是我们要解决的核心挑战。


构建系统的“神经中枢”:分层架构与实时操作系统的选型

任何复杂的嵌入式系统都离不开清晰的架构设计。如果把智能手表比作一个人,那么:

  • 底层驱动 是感官和肌肉(感知外界、执行动作);
  • 中间件服务 是神经系统(传递信号、协调反应);
  • 应用逻辑 是大脑皮层(做决策、处理信息);
  • UI框架 则是表情和语言(对外表达)。

为了实现这种分工明确、各司其职的设计理念,我们采用了典型的 四层分层架构

+---------------------+
|     用户界面 (UI)     |
+---------------------+
|   中间件服务层        |
+---------------------+
|  RT-Thread OS 核心   |
+---------------------+
|  底层硬件驱动         |
+---------------------+

在这个结构中,RTOS(实时操作系统)扮演着“脊髓”的角色——它不参与高级思考,但却能在毫秒级完成任务调度、中断响应和资源仲裁。

为什么选择 RT-Thread 而不是 FreeRTOS 或裸机编程?

✅ 开源活跃,社区支持好
✅ 组件丰富:文件系统、网络协议栈、电源管理一应俱全
✅ 支持动态模块加载,便于后期扩展
✅ 内存占用可控(最小可裁剪至3KB ROM + 1KB RAM)

更重要的是,RT-Thread 对国产芯片生态有天然适配优势,这对未来产品化非常关键。

// 系统初始化伪代码
void system_init() {
    clock_configure();        // 配置主频与低功耗时钟
    gpio_init();              // 初始化按键与指示灯
    rtthread_startup();       // 启动实时操作系统
}

⚡️ 设计哲学:“人机交互优先”,确保开机后300ms内进入UI框架,给用户“秒开”的体验感。


打通硬件血脉:外设驱动开发与接口整合

没有可靠的底层驱动,再漂亮的UI也只是空中楼阁。黄山派开发板提供了GPIO、I2C、SPI等标准接口,我们需要做的,就是把这些“电线”正确地连起来,并赋予它们生命。

GPIO、I2C、SPI 的资源规划与配置策略

先来看一组关键引脚分配表:

引脚编号 功能用途 工作模式 是否启用中断
PA0 按键输入 输入上拉
PB5 背光控制 输出推挽
PC7 传感器片选信号 输出普通
PD2 触摸控制器中断 输入下降沿触发

你可能会问:为什么要把触摸中断放在PD2?因为它支持外部中断线EXTI2,可以独立唤醒CPU,哪怕系统处于深度睡眠状态也能及时响应滑动手势 👆

而像SPI这样的高速总线,则要特别注意物理布局:

  • MOSI、MISO、SCK 必须使用同一端口组(如PB13~PB15),减少跨端口访问延迟;
  • 片选CS尽量靠近目标设备,避免信号反射;
  • 若带宽允许,优先启用DMA传输,解放CPU。
SPI 初始化实战
void spi_init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // 使能GPIOB时钟
    RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;   // 使能SPI2时钟

    // 配置PB13(SCK), PB15(MOSI)为复用推挽输出
    GPIOB->MODER   &= ~(0xFF << 26); 
    GPIOB->MODER   |= (0xAA << 26); 
    GPIOB->OTYPER  &= ~(0x3 << 13);
    GPIOB->OSPEEDR |= (0xF << 26);

    SPI2->CR1 = 0;
    SPI2->CR1 |= SPI_CR1_MSTR;             // 主机模式
    SPI2->CR1 |= SPI_CR1_BR_1;             // 波特率预分频=8 → 24MHz
    SPI2->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // 软件NSS管理
    SPI2->CR1 |= SPI_CR1_SPE;               // 使能SPI
}

💡 小贴士:若主频为96MHz,BR=0b011即对应 /8 分频 → 12MHz;但我们通过超频测试发现,在PCB走线较短的情况下,24MHz仍能稳定通信!这对提升屏幕刷新率至关重要。


显示屏驱动:ST7789 的通信协议实现

市面上大多数1.3英寸彩屏都采用 ST7789 作为驱动IC。它支持240×240分辨率,使用SPI接口通信,内部自带GRAM(图形RAM)。但它有个特点:命令和数据通过同一个SPI通道发送,靠一个 DC引脚 来区分。

简单来说:
- DC=0 → 下一条是命令
- DC=1 → 下一条是数据

所以我们需要封装两个基础函数:

// 写命令
void lcd_write_cmd(uint8_t cmd) {
    LCD_DC_LOW();
    spi_send(&cmd, 1);
}

// 写数据
void lcd_write_data(uint8_t *buf, uint16_t len) {
    LCD_DC_HIGH();
    spi_send(buf, len);
}

接下来就是按照数据手册执行初始化序列。以下是关键步骤摘要:

命令 参数序列 功能描述
0x11 退出睡眠模式
0x3A 0x05 设置色彩格式为16位RGB565
0x29 开启显示输出
0x2A 0x00,0x00,0x00,0xEF 设置列地址范围(X轴)
0x2B 0x00,0x00,0x01,0x3F 设置页地址范围(Y轴)

完整初始化函数如下:

void st7789_init(void) {
    lcd_reset();                     // 硬件复位
    delay_ms(120);

    lcd_write_cmd(0x11);            // 退出睡眠
    delay_ms(120);

    lcd_write_cmd(0x3A);            // 设置颜色模式
    uint8_t fmt = 0x05;
    lcd_write_data(&fmt, 1);

    lcd_write_cmd(0x2A);            // 列地址设置
    uint8_t col_addr[] = {0x00, 0x00, 0x00, 0xEF};
    lcd_write_data(col_addr, 4);

    lcd_write_cmd(0x2B);            // 行地址设置
    uint8_t row_addr[] = {0x00, 0x00, 0x01, 0x3F};
    lcd_write_data(row_addr, 4);

    lcd_write_cmd(0x29);            // 开启显示
}

⚠️ 常见坑点:如果不设置 0x3A 指令,默认可能是8位色深,导致画面发紫或错位!


触摸与传感器自检:让设备“自我诊断”

用户体验的好坏,往往取决于细节。比如你轻触屏幕却没有反应,第一反应不会是“SPI通信异常”,而是“这表坏了”。

因此我们在启动阶段加入了 上电自检流程 (Power-On Self Test, POST),确保每个模块都能正常工作。

XPT2046 触摸控制器检测
uint8_t touch_self_test(void) {
    uint16_t x_raw, y_raw;
    uint8_t pass_count = 0;

    for(int i = 0; i < 3; i++) {
        if(read_touch_coordinates(&x_raw, &y_raw)) {
            if(x_raw > 100 && x_raw < 4000 && 
               y_raw > 100 && y_raw < 4000) {
                pass_count++;
            }
        }
        delay_ms(10);
    }

    return (pass_count >= 2) ? 1 : 0;
}

这个函数做了三件事:
1. 连续读取三次坐标;
2. 判断是否在合理范围内(排除断线或噪声);
3. 至少两次有效才算通过。

类似地,加速度计 LIS2DH12 可通过读取ID寄存器验证连接状态:

uint8_t acc_device_check(void) {
    uint8_t id = 0;
    i2c_read_reg(LIS2DH12_I2C_ADDR, 0x0F, &id, 1);
    return (id == 0x33) ? 1 : 0; // 正常返回0x33
}

所有结果统一上报日志系统:

[INFO ][2024-04-05 10:12:01] Sensor init success
[ERROR][2024-04-05 10:12:03] I2C timeout @ 0x57

一旦发现故障,在UI层提示“传感器异常”,同时禁用相关功能,避免影响整体运行稳定性。


实时操作系统移植:让多任务并行不再是梦

裸机程序写多了就会遇到瓶颈:想一边采样心率,一边刷新屏幕,还要监听蓝牙消息……怎么办?轮询?那延迟太高了!

答案只有一个:引入RTOS。

RT-Thread 移植五步法

虽然官方尚未发布黄山派的完整BSP包,但我们可以通过手动移植搞定:

  1. 创建BSP目录结构
    bsp/huangshanpai/ ├── src/ ├── include/ └── linker_scripts/

  2. 编写启动文件 start.S
    - 设置堆栈指针
    - 清零 .bss
    - 跳转至 main

  3. 配置链接脚本

MEMORY
{
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text : {
        KEEP(*(.vector_table))
        *(.text)
    } > RAM

    .bss : {
        __bss_start = .;
        *(.bss)
        __bss_end = .;
    } > RAM
}
  1. 实现系统滴答定时器
void rt_hw_systick_init(void) {
    SysTick->LOAD = SystemCoreClock / 1000 - 1;
    SysTick->VAL  = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}
  1. 注册中断向量表
    确保 PendSV、SysTick 等指向 RT-Thread 调度器入口。

烧录后若串口输出 “RT-Thread kernel init success”,说明内核已成功启动 ✅


多任务调度模型设计

RTOS 的灵魂在于抢占式调度。我们为不同任务分配优先级:

任务名称 优先级 栈大小(字) 周期行为
GUI刷新 10 512 每33ms重绘一次
传感器采集 12 256 每20ms读取一次
BLE通信 15 1024 异步事件驱动
日志输出 20 128 非周期,按需触发

高优先级任务可打断低优先级任务执行。例如,当用户点击屏幕时,触摸处理任务(优先级14)会立即抢占GUI任务,实现流畅交互。

void sensor_task_entry(void *parameter) {
    while(1) {
        read_accelerometer();
        read_heart_rate();
        rt_thread_delay(RT_TICK_PER_SECOND / 50); // 20ms
    }
}

这里用的是 rt_thread_delay() ,单位是系统节拍。假设节拍频率为100Hz(每10ms一次),则除以50正好是20ms。


中断服务例程(ISR)与线程同步机制

中断是异步事件的基础,但在RTOS中必须小心处理。

❌ 错误做法:在中断里直接调用 printf() 或长时间运算
✅ 正确做法:只发事件,交给线程处理

void EXTI_IRQ_HANDLER(void) {
    if(__HAL_GPIO_EXTI_GET_IT(TP_INT_PIN)) {
        rt_event_send(&touch_event, TOUCH_PRESS_EVT);
        __HAL_GPIO_EXTI_CLEAR_IT(TP_INT_PIN);
    }
}

接收方这样等待:

rt_uint32_t recvd;
rt_event_recv(&touch_event, TOUCH_PRESS_EVT,
              RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
              RT_WAITING_FOREVER, &recvd);
handle_touch_input();

这种“中断→事件→线程”的解耦模式,极大提升了系统的健壮性和可维护性。


图形输出驱动:基于Framebuffer的双缓冲机制

尽管 ST7789 没有外部显存,我们仍可通过软件方式模拟帧缓冲区,避免画面撕裂。

#define FB_SIZE (240 * 240 * 2)  // RGB565,每像素2字节
static uint8_t framebuf[FB_SIZE];

void fb_draw_pixel(int x, int y, uint16_t color) {
    if(x >= 0 && x < 240 && y >= 0 && y < 240) {
        int idx = (y * 240 + x) * 2;
        framebuf[idx]     = color >> 8;
        framebuf[idx + 1] = color;
    }
}

void fb_flush(void) {
    lcd_set_area(0, 0, 239, 239);
    lcd_write_data(framebuf, FB_SIZE);
}

当然,这种方法占用了约115KB内存 💾。如果你的SRAM紧张,也可以改用局部刷新策略,只更新变化区域。


中间件服务层:打造系统的“神经系统”

如果说驱动是肢体,RTOS是脊髓,那么中间件就是真正的“大脑”——它负责协调各个模块之间的通信与协作。

系统事件总线:发布-订阅模式的实现

想象一下:心率传感器检测到异常,应该通知谁?
- UI要弹窗提醒
- 蓝牙要推送警报
- 存储模块要记录事件

如果每个模块都硬编码调用对方,代码将变得一团糟。

解决方案: 事件总线 (Event Bus)

我们使用 RT-Thread 提供的消息队列来实现:

typedef struct {
    event_type_t type;
    uint8_t data[MAX_EVENT_SIZE];
    uint32_t timestamp;
} system_event_t;

static rt_mq_t event_bus = RT_NULL;

void event_bus_init(void) {
    event_bus = rt_mq_create("evt_bus", sizeof(system_event_t), 16, RT_IPC_FLAG_FIFO);
}

任意模块都可以发送事件:

system_event_t evt = {
    .type = EVENT_TYPE_SENSOR_HRM,
    .data[0] = heart_rate_value,
    .timestamp = rt_tick_get()
};
rt_mq_send(event_bus, &evt, sizeof(evt));

而UI线程只需循环接收并分发:

while (1) {
    system_event_t evt;
    rt_mq_recv(event_bus, &evt, sizeof(evt), RT_WAITING_FOREVER);
    handle_event(&evt);
}

🎯 进阶技巧:对于高频传感器(如加速度计),不要频繁发送原始数据包,而是采用“共享缓存 + 数据就绪通知”机制,减轻总线压力。


时间与电源管理:续航的关键所在

智能手表最大的敌人不是性能不足,而是电量耗尽。

实时时钟(RTC)校准

即使使用高精度晶振,每天也会产生几秒误差。长期累积会影响闹钟准确性。

我们的对策是:定期通过蓝牙同步手机时间,并记录偏移量用于补偿。

static int32_t time_drift_ms = 0;

void apply_rtc_calibration(int32_t network_ms) {
    time_t local = get_current_timestamp();
    int32_t local_ms = local * 1000 + time_drift_ms;
    int32_t diff = network_ms - local_ms;

    time_drift_ms += diff;
}
屏幕休眠策略

屏幕是最大耗电源之一。我们设计了三级节能模式:

模式 平均电流
全亮显示 28mA
屏幕关闭 8mA
深度睡眠 1.2mA

并通过光线传感器自动调节亮度,形成闭环控制:

void check_ambient_light() {
    int lux = read_light_sensor();
    if (lux < 30) enable_night_mode();
    else disable_night_mode();
}

数据存储:EasyFlash 的轻量级KV方案

用户设置、主题偏好、健康数据都需要持久化保存。但Flash擦写寿命有限,不能随便乱写。

于是我们引入 EasyFlash ——一款专为嵌入式设计的KV存储库,支持磨损均衡和CRC校验。

ef_init();
ef_load_env();

if (!ef_get_env("screen_to")) {
    ef_set_env("screen_to", "30"); // 默认30秒
}

修改后记得批量提交:

ef_save_env(); // 减少Flash写入次数

用户界面构建:LVGL 让小屏也有大体验

终于到了最直观的部分:UI!

我们选择了 LVGL 作为图形引擎,原因很简单:

  • 开源免费(MIT协议)
  • 内存占用低(典型值:ROM 98KB + RAM 40KB)
  • 控件丰富,支持动画、抗锯齿、字体渲染
  • 社区活跃,文档齐全

LVGL 移植三步走

  1. 配置裁剪
#define LV_USE_ANIMATION     1
#define LV_FONT_MONTSERRAT   1
#define LV_USE_FILESYSTEM    0
#define LV_COLOR_DEPTH       16
#define LV_HOR_RES_MAX       240
#define LV_VER_RES_MAX       240

关掉不用的功能,节省资源。

  1. 注册显示驱动
static void disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
    st7789_set_window(area->x1, area->y1, area->x2, area->y2);
    st7789_write_data((uint8_t *)color_p, lv_area_get_width(area) * lv_area_get_height(area) * 2);
    lv_disp_flush_ready(disp);
}
  1. 注册触摸输入
static bool touch_read(lv_indev_drv_t *indev, lv_point_t *point) {
    if (xpt2046_read(point)) {
        point->x = 239 - point->x; // 校准翻转
        return true;
    }
    return false;
}

一切就绪后,就能用 lv_label_create() 创建第一个标签啦!


主界面设计:一眼获取关键信息

典型布局包含:

  • 中央时间区(大字号突出显示)
  • 上下状态栏(电量、蓝牙、时间)
  • 应用图标阵列(网格或轮播)

使用 Flex 布局轻松实现自适应排列:

lv_obj_set_flex_flow(app_menu, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(app_menu, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY);

还加入了点击波纹动画,提升交互质感:

lv_anim_set_exec_cb(&anim, [](void *obj, int32_t v) {
    lv_obj_set_style_bg_opa(obj, v, 0);
});

交互优化:让操作更自然

触控延迟测量

理想触控延迟应 < 100ms。我们通过时间戳差值法进行量化:

uint32_t touch_down_ts = 0;

static bool touch_read(...) {
    if (read_success && !lv_indev_get_button_state(NULL)) {
        touch_down_ts = lv_tick_get();
    }
    ...
}

void log_touch_latency() {
    uint32_t latency = lv_tick_get() - touch_down_ts;
    printf("Touch Latency: %ums\n", latency);
}

优化手段包括:
- 提高I2C读取频率至400kHz
- 给触摸任务更高优先级
- 使用预测性插值算法补偿采样延迟

盲操作反馈:震动提示

为增强操作确认感,我们集成了ERM电机提供触觉反馈:

void vibrate_pulse(uint16_t ms) {
    gpio_set_level(VIBRO_PIN, 1);
    lv_delay_ms(ms);
    gpio_set_level(VIBRO_PIN, 0);
}

经用户测试, 50ms短震 效果最佳:足够感知,又不会引起不适。


系统联调与测试:从原型到可用产品的跨越

开发中最痛苦的阶段往往是“明明单独测试都正常,合在一起就出问题”。

常见故障排查清单

故障现象 可能原因 解决方法
开机黑屏 显示驱动早于SPI初始化 调整初始化顺序
界面卡顿 高优先级任务霸占CPU 改为事件驱动,增加延时
传感器漂移 未校准或电源噪声 增加零偏补偿流程
蓝牙频繁断连 扫描窗口过短 调整 AT+SCANWIN=50
内存泄漏 LVGL对象未删除 使用 lv_obj_del() 及时释放

建议建立自动化测试脚本,模拟千次滑动、连续切换主题等场景,验证长期运行稳定性。


当前局限与未来升级方向

虽然原型已具备基本功能,但仍有不少改进空间:

🔋 电池续航优化

当前深度睡眠电流为1.2mA,相比商用产品(<0.5mA)仍有差距。下一步计划引入 DVFS (动态电压频率调节),根据负载自动降频MCU。

🧠 AI健康算法

目前步数统计依赖阈值判断,误判率高达17%。我们将尝试部署 TensorFlow Lite Micro ,训练轻量级LSTM模型识别走路、跑步、静止等状态。

💳 NFC支付

下一阶段重点拓展NFC功能,选用PN7150芯片,支持ISO14443标准,实现门禁卡模拟与小额支付。

☁️ 云端同步

通过MQTT协议将健康数据上传至私有服务器,支持跨设备查看趋势图表,打造完整的健康管理闭环。


写在最后:技术的价值在于落地

这款基于黄山派的智能手表原型,不只是一个“能跑起来”的demo,更是一套可复用的技术方案。

它验证了:
- RISC-V平台足以支撑复杂可穿戴系统
- RT-Thread + LVGL 是资源受限场景下的高效组合
- 分层架构 + 事件总线能让系统越做越清晰

更重要的是,我们总结出了一套 低功耗优化策略 驱动开发规范 调试方法论 ,这些经验完全可以迁移到其他项目中。

正如一位工程师所说:“最好的代码不是写得最多的,而是删得最多的。” ✂️

而这,正是嵌入式开发的魅力所在:在极限条件下,做出优雅而实用的系统。

🚀 下一站,量产见!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐