第04章 软件架构
约 976 字大约 3 分钟
2026-04-25
1 架构概述
本项目的软件架构与 NCS 中的 CAF(Common Application Framework)不同。CAF 采用"发布—订阅"事件总线模型,模块之间通过 Application Event Manager 通信。本项目由于功能相对简单、模块数量少,采用了更直接的分层 + 回调架构,代码路径更短、更易追踪。
| 对比维度 | CAF 架构 | 本项目架构 |
|---|---|---|
| 模块通信 | 事件总线 (发布-订阅) | 直接函数调用 + 回调 |
| 初始化顺序 | 各模块监听 main ready 事件自动初始化 | main() 中显式按序调用 |
| 复杂度 | 适合 10+ 模块的大型应用 | 适合 3~5 模块的小型应用 |
| 调试难度 | 事件流追踪需日志配合 | 函数调用栈直接可见 |
2 分层架构
┌─────────────────────────────────────┐
│ 应用入口 (main.c) │ ← 初始化调度、主循环、按键 ISR
├─────────────────────────────────────┤
│ Mesh 层 (mesh_app + models) │ ← BLE Mesh 协议栈、模型回调
├─────────────────────────────────────┤
│ 灯带输出层 (led_output) │ ← 状态管理、渲染刷新、线程安全
├─────────────────────────────────────┤
│ 灯效插件层 (effects registry) │ ← 灯效接口、注册表、具体灯效实现
├─────────────────────────────────────┤
│ 硬件抽象 (Zephyr 设备树 / 驱动) │ ← GPIO、SPI(WS2812)、BLE
└─────────────────────────────────────┘调用关系:
- main → led_output_init() → mesh_app_init() → sw1_init() → 主循环 render_step()
- Mesh 回调 → led_output_set_onoff/lightness/effect()
- 按键 ISR → worker → led_output_cycle_effect()
- led_output → effect_registry → 具体 effect->render()
3 并发模型
系统存在 3 个并发上下文 同时访问灯带状态:
| 上下文 | 线程 | 触发条件 | 操作 |
|---|---|---|---|
| 主线程 | main | 每 33ms 循环 | render_step()(读 + 输出 + 帧号+1) |
| BLE 回调线程 | BLE RX 线程 | 收到 Mesh 消息 | set_onoff / set_lightness / set_effect |
| 系统 workqueue | sys_workq | 按键 ISR 提交 work | cycle_effect |
同步策略:使用 k_mutex 互斥锁保护 led_output_state 全局结构体。所有读写操作内部加锁,持有锁的时间极短(仅做数值赋值/读取,不做 I/O)。
// 关键设计:渲染前快照,锁在渲染过程中释放
static void state_snapshot(struct led_output_state *snapshot) {
k_mutex_lock(&output_lock, K_FOREVER);
*snapshot = output_state;
k_mutex_unlock(&output_lock);
}4 灯效插件化机制
本项目的核心设计亮点——基于 Zephyr ITERABLE SECTION 的编译期自动注册。
原理
- 每个灯效
.c文件调用LED_EFFECT_REGISTER(var, id, name, render)宏 - 宏使用
STRUCT_SECTION_ITERABLE将描述符放入 linker 的led_effect_desc专有段 - 链接脚本
effects.ld中ITERABLE_SECTION_ROM收集所有该段条目,合并为连续数组 - 运行时
STRUCT_SECTION_FOREACH/STRUCT_SECTION_COUNT遍历查找
新增灯效只需 1 个文件
// src/effects/effect_new.c
#include "effects/effect_api.h"
static void render_new(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count) {
// ... 灯效渲染算法 ...
}
LED_EFFECT_REGISTER(effect_new, 4, "new", render_new);无需修改任何其他文件,编译后自动加入灯效列表。
灯效注册表 API
size_t effect_registry_count(void); // 灯效总数
const struct led_effect_desc *effect_registry_first(void); // 默认灯效
const struct led_effect_desc *effect_registry_get_by_id(uint8_t id);
const struct led_effect_desc *effect_registry_get_by_index(size_t idx);5 Mesh 模型结构
节点包含 1 个主元素,内含标准模型和自定义 Vendor 模型:
Element 1:
Model List 1 (主模型):
- Configuration Server (Mesh 网络配置,自动)
- Health Server (设备健康状态)
- Generic OnOff Server (开关控制)
- Light Lightness Server (亮度控制)
Model List 2 (Vendor 模型):
- Effect Server (自定义灯效切换)Vendor 模型参数:
- Company ID:
0x0059(Nordic) - Model ID:
0x0001 - 操作码:GET (
0xC10059)、SET (0xC20059)、SET_UNACK (0xC30059)、STATUS (0xC40059) - SET 消息格式:
[effect_id] [tid]
6 与 CAF 的关系
本项目未直接使用 CAF 框架,但借鉴了其设计思想:
- 模块化:effects / led / mesh 三层解耦,各自有明确的
.h接口 - 关注点分离:灯效算法不知道 Mesh 协议细节,Mesh 回调不知道灯带硬件细节
- 可扩展性:新增灯效就像 CAF 中新增 module 一样,只需关注自己的逻辑
如果后续功能增长到需要 10+ 模块(如加入 LCD 显示、电量管理、OTA 等),可考虑迁移到 CAF 架构以降低模块间的耦合度。
关于 CAF 的详细用法,可参考配套的蓝牙键盘项目文档。
