1 整体介绍

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

手表采用 Nordic nRF5340 芯片,配有圆形触摸屏、蓝牙和多种传感器;固件基于 Zephyr 和 LVGL,也支持开发自定义应用。项目主仓库采用 GPL-3.0 许可证。
硬件规格列表:
- nRF5340 BLE chip (u-blox NORA-B10 module)
- 128 MHz Dual core
- 512 KB RAM
- 1 MB Flash
- 30 MHz SPI for display
- 240x240 round display with touch screen
- IMU Bosch BMI270 for gesture navigation, wrist-wake, step counting
- Bosch BMP581 High-performance pressure sensor (~20 cm accuracy)
- ST LIS2MDLTR Magnetometer
- Macronix MX25U51245GZ4I00 64 MB external flash
- Broadcom APDS-9306-065 Light Sensor for automatic brightness control
- Micro Crystal RV-8263-C8 RTC for time keeping and alarm functions
- Knowles SPK0641HT4H-1 I2S microphone for audio recording
- Nordic nPM1300 PMIC for power and system management
- Speaker (Requires HR Extension Board)
硬件框图:

可以看出,使用了一个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 省电 ------ 这些功耗优化的经验值都写在代码注释里,读一遍收获很大。
