嵌入式架构学习6--ZSWatch(TODO)

1 整体介绍

主要特点就是软硬件开源,并且使用了Zephyr系统。ZSWatch名字来自它使用的 Zephyr RTOS。项目包含手表硬件设计、固件和开发文档,

手表采用 Nordic nRF5340 芯片,配有圆形触摸屏、蓝牙和多种传感器;固件基于 Zephyr 和 LVGL,也支持开发自定义应用。项目主仓库采用 GPL-3.0 许可证。

硬件规格列表:

硬件框图:

可以看出,使用了一个nRF53的SoM(关于SoM,可以参考我之前的文章:SoM的理解_soc和som-CSDN博客)。搭配了非常多外围Sensor。

总的来说,这个项目用来学习Zephyr,BLE,传感器,音频流和低功耗,感觉还是挺不错的。。。

2 Zephyr架构

2.1 目录结构

在代码中也创建了给AI的md文件,主要内容如下:

bash 复制代码
```
app/                        # Main application (west manifest repo)
├── CMakeLists.txt          # Top-level CMake
├── Kconfig                 # Top-level Kconfig
├── prj.conf                # Main project configuration
├── west.yml                # West manifest (NCS v3.1.0)
├── boards/                 # Board definitions and overlays
│   └── zswatch/            # ZSWatch board definitions
│       ├── watchdk/        # Watch DevKit (current board)
│       └── zswatch_legacy/ # Legacy hardware (v4/v5)
├── src/
│   ├── main.c              # Application entry point
│   ├── applications/       # All watch apps (self-registering)
│   ├── managers/           # System managers (app, power, notification, etc.)
│   ├── events/             # Zbus event definitions and channels
│   ├── ble/                # BLE services (GadgetBridge, AMS, ANCS, CTS, etc.)
│   ├── sensors/            # Sensor drivers and abstractions
│   ├── sensor_fusion/      # Sensor fusion algorithms
│   ├── ui/                 # UI components (app picker, popups, watchfaces)
│   ├── drivers/            # Custom Zephyr drivers
│   ├── images/             # Image assets
│   └── fuel_gauge/         # Battery fuel gauge
├── lvgl_editor/            # LVGL Editor integration (XML-based UI design)
├── drivers/                # Out-of-tree Zephyr drivers
└── dts/                    # Device tree overlays and bindings
website/                        # zswatch.dev documentation website (Docusaurus 3.7)
├── docusaurus.config.js    # Site config (URL, navbar, footer, plugins)
├── sidebars.js             # Sidebar auto-generated from docs/ structure
├── package.json            # Node.js dependencies
├── tailwind.config.js      # Tailwind CSS configuration
├── docs/                   # Documentation pages (Markdown/MDX)
│   ├── intro.md
│   ├── getting-started/    # Quickstart, hardware, software setup
│   ├── development/        # Compiling, toolchain, image resources, Linux dev
│   ├── hardware-design/    # PCB design documentation
│   └── hw-testing/         # Hardware testing procedures
├── blog/                   # Blog posts
├── src/
│   ├── components/         # Custom React components
│   │   ├── FirmwareUpdate/ # Web-based firmware update component
│   │   ├── GithubNavbar/   # GitHub stars navbar widget
│   │   ├── HomepageFeatures/
│   │   └── SubscriptionForm/
│   ├── pages/              # Custom pages (index, update, etc.)
│   ├── theme/              # Docusaurus theme overrides
│   └── css/                # Custom CSS (Tailwind)
└── static/                 # Static assets (images, favicon, etc.)
```

主要的开发内容如下,可以看到基本上都是app下面。

目录 体积 作用
app/ 99M 主固件工程 ,也是 west 的 manifest 仓库,west.yml 在这里。下面单独展开
hardware/ 680M 4 个 KiCad 硬件子模块:Watch-HW(手表 PCB)、Watch-DevKit-HW(开发板)、Heartrate-HW(心率/音频扩展板)、Dock-HW(调试扩展坞)。跟固件开发基本无关
website/ 64M Docusaurus 站点 zswatch.dev 的源码(docs/、blog/),含 mcumgr-web 子模块(网页版 OTA 工具)
resources/ 1.3M Logo SVG、硬件总览图、配色 palette.pdf、ZSWatch.drawio
boards/ 完整的 BSP(板级支持包)
app/drivers/ 实现了 display(gc9a01 圆屏)、input(nPM1300 按键)、sensor(apds9306 光感 / bmi270 / bmp581)、audio(dmic 模拟器)。这些进的是 zephyr_library(),提供标准子系统 API(display_blanking_on()、sensor_sample_fetch()...)。

board内容如下:

文件 作用
zswatch_nrf5340_cpuapp.dts / _ns.dts / _cpunet.dts + *_common.dtsi + *-pinctrl.dtsi 设备树:描述硬件连接、引脚复用(pinctrl)、分区
*_defconfig 板级默认 Kconfig(开 TrustZone、MPU、关串口 console...)
board.yml / *.yaml + Kconfig / Kconfig.defconfig / Kconfig.zswatch hwmv2 板元数据 + 板级 Kconfig 选项
board.cmake 烧录器配置 :J-Link 的 --device=nrf5340_xxaa_app、nrfjprog 的外部 flash ini
pre_dt_board.cmake 编译设备树之前的特殊处理(这里用来压制地址冲突告警)
support/ + nrf5340_cpunet_reset.c 外部 flash 编程参数、网络核复位代码

还有一个drivers,app/src/drivers/ 它是板级适配/封装层,就是普通 C 函数,它标准驱动 API。直接编进 api。

2.2 代码架构

主要的代码是在src中,包含了下面五类:

  • 应用 :applications/(30+ 手表 App)
  • 中间件/服务 :managers/(电源/通知/应用管理)、events/(zbus 频道)、ui/(UI 控制器与组件)、filesystem/、history/、sensor_fusion/
  • 领域封装 :sensors/(把 Zephyr sensor API 包成手表语义,如 zsw_imu_fetch_num_steps())、codec/、fuel_gauge/、ble/
  • 板级适配 :src/drivers/(见上)
  • 基础设施 + 资源 :根目录的 zsw_clock/settings/alarm/coredump/wdt/cpu_freq/retained_ram,以及 ext_

3 ZBus

3.1 Zbus定义

zbus 是 Zephyr 自带的 消息总线(不是第三方库),ZSWatch 把它用成了整个固件的神经系统。先看它最重要的一条性质------一对多广播,发布者完全不认识订阅者

zbus 是什么

它是 Zephyr 内核自带的发布-订阅消息总线 (CONFIG_ZBUS=y),核心就两个概念:

  • Channel(频道) :一个带类型的消息槽,比如 struct accel_event 类型的 accel_data_chan
  • Observer(观察者) :订阅频道的实体,分两种 ------ Listener (同步回调,在发布者的上下文里立刻执行)和 Subscriber(有自己的消息队列,异步慢慢处理)

在ZSWatch项目只用了Listener (搜不到任何 ZBUS_SUBSCRIBER_DEFINE),因为手表事件大多是"轻处理 + 立刻响应"。

下面是在Zephyr框架中,标准的ZBus用法。

cpp 复制代码
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/zbus/zbus.h>

/* 1) 消息类型:频道里传的东西 */
struct sensor_data {
    int32_t value;
};

/* 2) Listener 回调:同步执行,跑在发布者的上下文里 */
static void listener_cb(const struct zbus_channel *chan)
{
    const struct sensor_data *d = zbus_chan_const_msg(chan);
    printf("  [listener]   %s -> value=%d\n", chan->name, d->value);
}

/* 3) 另一个 Listener:运行期间再挂上去 */
static void late_listener_cb(const struct zbus_channel *chan)
{
    const struct sensor_data *d = zbus_chan_const_msg(chan);
    printf("  [late]       %s -> value=%d\n", chan->name, d->value);
}

/* 4) 定义观察者:两个 Listener + 一个 Subscriber(队列长度 4) */
ZBUS_LISTENER_DEFINE(my_listener, listener_cb);
ZBUS_LISTENER_DEFINE(my_late_listener, late_listener_cb);
ZBUS_SUBSCRIBER_DEFINE(my_subscriber, 4);

/* 5) Subscriber 是异步的:需要自己的线程去取消息 */
static void subscriber_thread(void *a, void *b, void *c)
{
    const struct sensor_data data;
    const struct zbus_channel *chan;

    while (zbus_sub_wait(&my_subscriber, &chan, K_FOREVER) == 0) {
        zbus_chan_read(chan, &data, K_MSEC(100));
        printf("  [subscriber] %s -> value=%d\n", chan->name, data.value);
    }
}
K_THREAD_DEFINE(sub_tid, 2048, subscriber_thread, NULL, NULL, NULL, 5, 0, 0);

/* 6) 频道定义:编译期静态绑定 my_listener 和 my_subscriber */
ZBUS_CHAN_DEFINE(sensor_chan,
                 struct sensor_data,
                 NULL, NULL,
                 ZBUS_OBSERVERS(my_listener, my_subscriber),
                 ZBUS_MSG_INIT(0));

int main(void)
{
    struct sensor_data d = {0};

    printf("=== 1. 基础发布(1 listener + 1 subscriber)===\n");
    for (int i = 1; i <= 3; i++) {
        d.value = i * 10;
        printf("[publisher] pub %d\n", d.value);
        zbus_chan_pub(&sensor_chan, &d, K_MSEC(100));
        k_msleep(50);
    }

    printf("=== 2. 运行时追加观察者 ===\n");
    zbus_chan_add_obs(&sensor_chan, &my_late_listener, K_MSEC(100));
    d.value = 99;
    printf("[publisher] pub %d\n", d.value);
    zbus_chan_pub(&sensor_chan, &d, K_MSEC(100));

    printf("=== 3. 主动拉取当前值(不等事件)===\n");
    struct sensor_data cur;
    zbus_chan_read(&sensor_chan, &cur, K_MSEC(100));
    printf("[pull] current value = %d\n", cur.value);

    printf("=== 4. 移除运行时观察者 ===\n");
    zbus_chan_rm_obs(&sensor_chan, &my_late_listener, K_MSEC(100));
    d.value = 123;
    printf("[publisher] pub %d\n", d.value);
    zbus_chan_pub(&sensor_chan, &d, K_MSEC(100));

    k_msleep(200);
    printf("=== done ===\n");
    return 0;
}

3.2 ZSWatch的用法

所有频道集中在 src/events/ 一个目录,一频道一对 .c/.h

频道 承载 典型订阅者
accel_data_chan 抬手/计步/手势/静止 表盘、电源管理
activity_state_data_chan 手表活动状态三态 表盘、PMIC、应用管理器
ble_comm_data_chan 手机发来的数据 通知管理、时间同步、表盘、音乐(4 个)
battery_sample_data_chan 电量采样 表盘、电量 App、手机同步
zsw_notification_mgr_chan 通知到达/移除 通知 App、main
periodic_event_1s/100ms/10s_chan 周期心跳 时钟、传感器轮询、秒表
light/pressure/magnetometer_data_chan 传感器数据 按需订阅
music_control_data_chan 音乐控制 Android / iOS 各一个实现

下面一个磁力的定义

app/src/events/magnetometer_event.h

cpp 复制代码
#include "sensors/zsw_magnetometer.h"

struct magnetometer_event {
    float x;
    float y;
    float z;
};

app/src/events/magnetometer_event.c

cpp 复制代码
#include <zephyr/zbus/zbus.h>

#include "magnetometer_event.h"

ZBUS_CHAN_DEFINE(magnetometer_data_chan,
                 struct magnetometer_event,
                 NULL,
                 NULL,
                 ZBUS_OBSERVERS_EMPTY,
                 ZBUS_MSG_INIT()
                );

3.3 一个例子

以「抬手亮屏」:IMU 中断 → zbus 事件 → 电源管理 → 显示控制 → 屏幕驱动。来说明整个事件流程。

前半段:

硬件中断 → 统一事件结构 → zbus 广播。

后半段:

电源管理器收到后做决策,并且只在息屏状态下才响应(避免重复亮屏)。

可以看出,使用ZBus之后,就将事件的产生和使用分开了。。

4 BLE

4.1 BLE框架

nRF5340 是双核,所以 HCI 不走串口也不走 USB,而是走核间 IPC。网络核跑 Nordic 的 SoftDevice Controller,应用核跑完整的Zephyr BLE Host。

代码中涉及到BLE的大概是这些代码。

组织如下:

层 文件 职责
传输层 ble_transport.c(96 行) 只做一件事:NUS 服务(Nordic UART Service,6e400001-...)。一个 RX(write-without-rsp)+ 一个 TX(notify)+ CCC
协议层 gadgetbridge/(1377 行)、chronos/(906 行) 两套并存的手机 App 协议,各自解析

服务与客户端层

文件 作用
ble_ancs.c/.h iOS Apple 通知中心服务客户端 。手表反过来做 GATT Client 连 iPhone,用 bt_gatt_dm 做服务发现,订阅 Notification Source / Data Source,解析出标题、正文、App 名后 zbus_chan_pub(&ble_comm_data_chan)。带发现失败重试(K_WORK_DELAYABLE)
ble_ams.c/.h Apple 媒体服务客户端 。订阅 iOS 的曲目/播放状态,同时反向订阅 music_control_data_chan ------ 手表上点播放/切歌时,通过 AMS 写回 iPhone
ble_cts.c/.h Current Time Service 客户端。连上 iPhone 后读一次当前时间(含时区偏移)用于自动对时,也订阅时间变化通知
zsw_gatt_sensor_server.c/.h 自定义传感器 GATT 服务 。温度/湿度/气压/加速度/陀螺仪/磁力计/光照/传感器融合各一个独立服务,UUID 用 Adafruit 的 ADAFxxxx (这样 Adafruit Bluefruit 生态的 App 能直连读数据)。有意思的是它不订阅传感器频道 ,而是挂 periodic_fast 心跳,按 notify_period_counter 分频、且只在客户端订阅了 CCC 时才发通知(bt_gatt_is_subscribed 检查)------没人听就完全不占带宽

连接与工具

文件 作用
ble_comm.c/.h BLE 子系统的中枢 。ble_comm.h 定义了整个模块的通用语言:ble_comm_data_type_t 枚举(notify/set_time/weather/music/http/gps...)+ 各种载荷结构体 + 联合体 ble_comm_cb_data_t。.c 负责连接生命周期(BT_CONN_CB_DEFINE)、配对鉴权(bt_conn_auth_cb)、广播快慢切换、连接间隔切换、MTU 交换、按 MTU 分片发送 。也是 bt_receive_cb 所在处------把一包数据同时喂给 GB 和 Chronos 两个 parser
ble_http.c/.h HTTP-over-BLE 代理 。zsw_ble_http_get(url, cb) 向手机发 HTTP 请求,订阅 ble_comm_data_chan 等 {t:"http"} 的应答,带超时(ble_http_timeout_handler)。手表本身没有 Wi-Fi,上网全靠这个
ble_log_backend.c/.h Zephyr 日志后端 。注册成 struct log_backend,把 LOG_* 输出经 BLE 发给手机(配合官方 App 的开发者工具)。连上/断开时自动启停,可 ble_log_backend_set_enabled() 开关
ble_hid.c/.h HID over GATT (HOGP)。让手表变成蓝牙键盘,目前只实现 ble_hid_next() / ble_hid_previous()------给"PPT 翻页遥控器"应用用。​条件编译 :只有开了 CONFIG_APPLICATIONS_USE_PPT_REMOTE 才编进去
ble_aoa.c/.h 到达角(AoA)定位广播 。用 BLE 5.1 的 CTE(Constant Tone Extension)发定向广播,让外部定位基站算出手表方位。代码版权是 u-blox 2022 (Apache 2.0),用的 Eddystone + NINA-B4TAG 命名空间 ------ 明显是从 u-blox 的 u-connectLocate 方案移植来的。注意整个文件包在 #ifdef CONFIG_BT_DF 里,默认没开

有意思的有三个,苹果的服务,手表做蓝牙键盘,还有一个蓝牙定位。。。空了看看。

4.2 蓝牙的流程

蓝牙处理的流程如下:

  • 启动与广播

    main.c 调用 bt_enable(),加载蓝牙设置后执行 ble_comm_init();后者注册连接与配对回调、初始化传输服务,并开始 BLE 广播。默认广播间隔较慢;建立连接后会先请求较短的连接间隔,数秒后再放慢以节省功耗。

    位置:app/src/main.c:176-194、app/src/ble/ble_comm.c:166-180, 350-425

  • 手机与手表之间的通用数据通道

    ble_transport.c 定义了与 Nordic UART Service 兼容的 128 位 UUID:手机向 RX 特征写入数据,手表通过 TX 特征的通知发数据。连接后会协商 MTU,再按协商出的最大负载长度拆分发送。

    位置:app/src/ble/ble_transport.h、app/src/ble/ble_transport.c:29-95、app/src/ble/ble_comm.c:157-180, 350-375

  • 应用协议和事件分发

    收到的字节会同时交给 Gadgetbridge 和 Chronos 解析。Gadgetbridge 使用 GB({...}) 消息框架,支持通知、天气、音乐状态、HTTP、GPS 等消息;解析出的数据通过 Zephyr zbus 事件通道分发给通知管理器、音乐界面等模块。

    位置:app/src/ble/ble_comm.c:469-476、app/src/ble/gadgetbridge/ble_gadgetbridge.c:1014-1193、app/src/events/ble_event.c

  • 传感器 GATT 服务

    代码还暴露温度、加速度、湿度、气压、磁力计、陀螺仪、融合姿态和光照等服务。客户端订阅 CCC 通知后,手表启动周期任务,大约每 200 ms 推送一次传感器数据;取消订阅后会停掉相关任务和传感器功能。

    位置:app/src/ble/zsw_gatt_sensor_server.c:65-150, 258-388

  • 配对与权限

    app/prj.conf 启用了 SMP、蓝牙设置持久化,并把最大连接数设为 1。默认 GATT 特征要求链路加密;项目也提供调试配置,可以关闭配对加密要求。配对确认默认拒绝,代码可切换为允许配对。

    位置:app/prj.conf:171-210、app/src/ble/ble_comm.c:130-164、app/src/ble/ble_transport.c:45-70

4.3 Gadgetbridge和chronos

这里说一下Gadgetbridge和chronos。基本上就是为了白嫖两个app。。。

Gadgetbridge:开源安卓手表 App

gadgetbridge.org上的 FOSS Android 应用,F-Droid 可装。不过这个库只有安卓的版本。

Chronos:DIY ESP32 手表生态的 App

chronos.ke 的 Chronos 手机 App(Android + iOS 都有),原本配套 ChronosESP32 这个 Arduino 库 ------ 给爱好者用 ESP32 自己做智能手表用的。

后面官方除了app之后,目前是这样的:

通道 平台 定位
ZSWatch 官方 App(Flutter) Android + iOS 功能最全:DFU 升级、LVGL 资源上传、HTTP 代理、GPS 中继、日志/传感器开发工具
GadgetBridge 仅 Android 开源无云,官网列为推荐,后台连接最稳
Chronos Android + iOS 第三方生态兼容,早期主力
ANCS / AMS 仅 iOS 通知和音乐根本不需要 App,系统原生

为了兼容Gadgetbridge和chronos,所以直接在BLE的代码中做了支持。

本质可以参考之前的文章:蓝牙学习3(简易BLE GATT)-CSDN博客

就像这里的代码,1是开灯0是关灯。这里的这两个其实是一样。。。

5 Audio

5.1 整体流程

这里绿色是录音链,橙色是播放链,灰色是共享存储。

录音管线(麦克风 → Opus → 文件系统)

播放管线(I2S → 编解码器 → 扬声器)。

5.2 录音流水线

代码在 app/src/ 下共 12 个文件、约 2400 行,另外还有一个出树驱动 app/drivers/audio/dmic_emul.c。

5.2.1 MIC

MIC是楼氏(Knowles)SiSonic™ 系列的一颗高端数字 MEMS 麦克风,完整型号是 SPK0641HT4H-1,输出是单 bit PDM。它是 "数字 PDM 麦",不是 I2S 麦------ WM8960/MAX98357A 这类 I2S Codec 一般不能直接接它。要接它,主控必须有PDM 输入通道(很多 SoC 自带)。没有寄存器、没有 I2C/SPI 地址、没有中断脚、没有 GPIO。既然没有任何东西可读写,它在 Zephyr 的驱动模型里就不是一个"设备"------设备树里没有节点,自然也不需要驱动。

对外只有两根信号线:

  • MCU 给它 一个时钟(1--3.25 MHz)
  • 它回 一串 1-bit 的脉冲密度流(密度代表声压)

只在DTS有个描述nrf5340_cpuapp_common-pinctrl.dtsi。

bash 复制代码
pdm0_default: pdm0_default {
    group1 {
        psels = <NRF_PSEL(PDM_CLK, 0, 25)>,    // P0.25 出时钟给麦克风
                <NRF_PSEL(PDM_DIN,  0, 26)>;   // P0.26 收 1-bit 数据流
    };
};

5.2.2 采样与抽取

代码在zephyr/drivers/audio/dmic_nrfx_pdm.c,DT_DRV_COMPAT = nordic_nrf_pdm。

这是 SoC 的 PDM 控制器驱动,不是 SPK0641 的驱动。它基于 nrfx_pdm,用 nRF5340 内部的硬件抽取滤波器把 1-bit PDM 流变成 16-bit PCM。

整体还有一个src/drivers/zsw_microphone.c。做三件事:上电、配置 PDM、起线程取数据。用 Zephyr 标准 dmic_configure() / dmic_trigger() / dmic_read(),然后自己起一个线程(1024 栈)循环读:

cpp 复制代码
while (mic_state.recording) {
    ret = dmic_read(mic_state.mic_dev, 0, &rx_block_ptr, &rx_size, 100);
    if (ret == -ENOMEM) { buffer_allocation_failures++; k_msleep(10); continue; }
    mic_state.audio_callback(rx_block_ptr, rx_size);
    k_mem_slab_free(&rx_mem_slab, rx_block_ptr);   // 回调里必须同步用完
    k_yield();
}

5.2.3 录音管理器

整体的录音管理,调用录音的接口实现功能。代码是在zsw_recording_manager.c。实现了生产者和消费者。

同时实现 Opus 编码:src/codec/zsw_audio_codec.c。这里是标准 Opus 封装,但有个 ZSWatch 特有的关键点------编码器跑在外部flash的XIP上。

XIP = eXecute In Place,就地执行 ------CPU 直接从存储器里取指令运行,不用先把代码拷贝到 RAM。

最后使用了自己的文件系统,将录音保存成文件,放在手表闪存的 /user/recordings 目录。

5.2.4 应用

代码仓中还有一些录音的应用,我就没有细看了。。。

  • Mic App (applications/mic/):用 kissfft 做 64 点实数 FFT ,攒满样本算一次,结果经 k_work_submit 交给主线程更新圆形频谱 UI。​FFT 在采集线程里做,UI 更新切出去------因为 LVGL 不是线程安全的。
  • Voice Memo (applications/voice_memo/):录音列表、时长、电平条,配 shell 命令(录制/列表/删除/空间查询),还有 ui/overlay/zsw_quick_record.c(长按按键快速录音)和 zsw_recording_overlay.c(录音中悬浮窗)。

5.3 播放流水线

代码是在zsw_speaker_manager.c。基本就是一个I2S的播放,不过之前的硬件规格就介绍了,Speaker (Requires HR Extension Board),也就说现在板载是没有硬件喇叭的。

所以整个功能还是未完成的阶段,暂时就不细看了。

6 Display

TODO

7 Sensor

之前硬件规格就说了,ZSWatch硬件上有四颗传感器,全挂在 i2c1。

  • IMU Bosch BMI270 for gesture navigation, wrist-wake, step counting
  • Bosch BMP581 High-performance pressure sensor (~20 cm accuracy)
  • ST LIS2MDLTR Magnetometer
  • Broadcom APDS-9306-065 Light Sensor for automatic brightness control

定义在:app/boards/zswatch/zswatch/nrf5340_cpuapp_common.dtsi:270-306:

器件 地址 中断 备注
BMI270 0x68 GPIO0.6,上升沿 六轴 + 特性引擎,swap-xy + invert-y 做坐标系对齐
BMP581 0x47 --- 气压 / 温度
LIS2MDL 0x1e GPIO1.7 三轴磁力计,cancel-offset
APDS9306 0x52 --- 环境光

7.1 驱动

  • BMI270 (app/drivers/sensor/bmi270/,1999 行):最重的一块。bmi270_init() 会先把 8 KB 的 bmi270_config_file[](在 ext_drivers/BMI270-Sensor-API/bmi270.c:51)烧进芯片------BMI270 内部是带特性引擎的可编程 ASIC,不灌这个文件就只有裸加速度计功能。之后 private/zsw_bosch_bmi270_config.c 用一张 feature_config_func 表把 8 个特性(加速度/陀螺/计步/活动识别/手势/抬手唤醒/静止/任意运动)逐个配置。
  • BMP581 (zswatch,bmp581):项目自制。
  • LIS2MDL / APDS9306 :直接用 Zephyr 上游驱动(st,lis2mdl / avago,apds9306),项目只补了 emul 版本给 native_sim 仿真用。

7.2 应用层

主要是app/src/sensors下面的代码

这里主要是对传感进行一个管理,同时给更上层提供接口。同时发zbus,把事件推到对应频道。

  • 向下 :使能/挂起(pm_device_action_run)、配置(ODR、量程、特性开关)、校准(磁力计 hard-iron 偏移 + settings 持久化)、仲裁(引用计数)
  • 向上 :同步查询(zsw_imu_fetch_accel_f() 这种)+ 异步事件(zbus_chan_pub)

7.3 传感器融合

项目中使用了三个传感器,但是各有各的致命伤,而且伤的地方正好互补:

  • 陀螺仪:响应快、短期极准,但积分漂移。零偏哪怕只有 0.1°/s,一分钟就是 6°,而且随温度变。
  • 加速度计 :能测重力方向,所以能定 roll 和 pitch,长期不漂。但两件事要命------① 任何运动加速度都叠加在上面(走路摆臂、抬手、敲桌子),② 物理上它定不了 yaw:重力始终竖直向下,你绕重力轴转多少度,加速度读数一模一样。
  • 磁力计:能补 yaw(这就是 AHRS 里那个 H = Heading)。但环境畸变严重。

传感器的用途:

只用倾角的(不需要磁力计、不需要融合):

  • 抬手亮屏(判断表盘是否转向脸)
  • 垂手/转腕息屏
  • 手表是否在腕上(off-wrist detection,防止脱腕后计步乱计)
  • 睡眠翻身检测
  • 屏幕方向切换(部分 Wear OS 表)

需要完整姿态的:

  • 指南针、地图方向箭头
  • 跌倒检测(冲击波形 + 姿态突变,漏报是要命的)
  • 游泳划水识别、高尔夫挥杆分析
  • 手势控制(翻腕切歌、双击)
  • 3D 表盘、AR、隔空控制手机/无人机

代码是src/sensor_fusion/zsw_sensor_fusion.c。使用了一个核心算法是Madgwick博士论文第 7 章的"修订版 AHRS"。不过代码中并未实际使用。。。

Madgwick 算法是 Sebastian Madgwick 在 2011 年提出的惯性姿态解算算法 ,用于把 IMU(加速度计 + 陀螺仪,可选磁力计)融合成实时姿态四元数:它用梯度下降法 迭代优化四元数,使陀螺仪积分出的方向 (响应快但随时间漂移)与加速度计 / 磁力计提供的地球重力与磁场参考方向(无漂移但噪声大)相融合,用一个可调增益 β 平衡两者权重,从而同时获得快速响应和长期稳定,且计算量远小于卡尔曼滤波,能在普通 MCU 上实时运行,因此被大量用于无人机、穿戴设备、机器人等的姿态估计(AHRS/IMU)。

Madgwick 修订版(Revised AHRS, 2012) 是在原版算法(2011)基础上的官方改进版,用于惯性 / 磁惯性姿态估计:它保留了 "陀螺仪负责快速积分、加速度计与磁力计提供无漂移参考" 的融合框架,但把原来单一的 β 增益换成了比例--积分(PI)反馈结构 ,用可调增益 kp/ki 把 "测量参考与实际姿态之间的误差" 经比例--积分后作为修正项叠加回陀螺仪积分 ,从而构成一个闭环、能持续抑制漂移的估计器;同时它在线估计并反馈消除陀螺仪偏置(bias) ,并重新设计了磁力计的倾斜补偿处理,显著改善了磁力计与加速度计融合时的偏航精度及磁场干扰下的稳健性,避免原版在特定运动 / 磁场环境下四元数发散,而计算量仍保持在普通 MCU 可实时运行的水平。

100 Hz 自调度的 k_work_delayable(1000/SAMPLE_RATE_HZ - 本次耗时),把 rad/s 转 deg/s、m/s² 转 g 后喂给 AHRS,输出欧拉角 + 四元数 + 地系加速度。CONFIG_SENSOR_FUSION_INCLUDE_MAGNETOMETER 决定走 FusionAhrsUpdate() 还是 FusionAhrsUpdateNoMagnetometer()(默认 关,因为磁力计未校准时会把姿态带歪)。init/deinit 同样是引用计数,进 Compass 开陀螺+唤醒磁力计,退出还原。

8 其它

8.1 低功耗

低功耗这边这条链上最值得学的四点:

1 算法放在传感器里:抬手/计步/手势全是 Bosch BMI270 内置 feature,用 zsw_imu_feature_enable/disable()(带引用计数)开关,MCU 可以真睡,只等中断。

2 XIP 和显示状态强耦合:因为 LVGL 字体和不常用控件被重定位到外部 flash 上执行,息屏才能关XIP省电,唤醒必须第一步开 XIP ------ 顺序写反就会跑飞。

3 动态 CPU 频率:渲染时128MHz(注释解释了"32MHz SPI 需要 128MHz CPU"),息屏降回默认。

4 注释即设计文档:等 100ms、延迟开背光、关 PWM 省电 ------ 这些功耗优化的经验值都写在代码注释里,读一遍收获很大。

8.2 APP

相关推荐
叼烟扛炮1 小时前
Linux 学习笔记(十三):高效排错利器——GDB 调试器入门
linux·笔记·学习
AI职业加油站2 小时前
模数共振行动解读|大模型应用工程师证书,匹配行业 RAG/Agent 落地人才需求
人工智能·学习·算法·职场和发展·职场发展
SZleoWang2 小时前
费曼学习法的4个步骤:选概念、教别人、找缺口、再简化
学习
lengxuenong3 小时前
树状数组 · 信息学学习手册2-离散化与逆序对
c++·学习·算法
minglie13 小时前
订阅莫名变少的奇怪现象
学习
zhangrelay4 小时前
手机长周期测评十周年终结版本-2026
笔记·学习·智能手机
boy快快长大4 小时前
【Jenkins】入门学习
运维·学习·jenkins
m4Rk_5 小时前
【论文阅读】Agent记忆机制(106):COUNTERMEM——用可执行世界模型验证“如果当时换个动作”的反事实记忆
论文阅读·人工智能·学习·开源·github
测试运维日常笔记5 小时前
SQL 快速学习笔记:1~2 小时掌握面试重点
笔记·sql·学习