第05章 项目实现
约 6811 字大约 23 分钟
2026-04-25
按照第03章创建好空项目和 BSP 后,我们分 4 个模块逐步实现固件功能。每个模块有独立的需求分析、硬件/BSP 配置、代码实现和验证方案。
实施顺序:
模块1: 让灯带先亮起来 (最简LED输出层,验证硬件)
→ 模块2: 搭建灯效框架+实现所有灯效 (effect_api + 4个灯效 + 重构led_output)
→ 模块3: 加上本地按键 (sw1 ISR+worker)
→ 模块4: 加上 Mesh 远程控制 (mesh_app+mesh_models)每个模块完成后可独立验证——不需要等全部写完才看到效果。
1 整体项目架构
1.1 模块依赖关系
main.c
├── led_output (灯带输出层) ← 所有控制的汇聚点
│ ├── effect_registry (灯效注册表) ← 编译期自动收集灯效
│ │ └── effect_api.h (灯效接口) ← 标准渲染函数签名
│ │ ├── effect_solid.c ← 常亮 (id=0)
│ │ ├── effect_flow.c ← 流动 (id=1)
│ │ ├── effect_breathe.c ← 呼吸 (id=2)
│ │ └── effect_rainbow.c ← 彩虹 (id=3)
│ └── Zephyr LED Strip 驱动 (SPI→WS2812)
├── mesh_app (Mesh 初始化 + UUID)
│ └── mesh_models (模型组合 + 回调)
│ ├── Generic OnOff Server ← 开关 (SIG 标准模型)
│ ├── Light Lightness Server ← 亮度 (SIG 标准模型)
│ └── Vendor Effect Server ← 灯效切换 (自定义)
└── sw1_init (本地按键 → 灯效循环)1.2 数据流
Mesh 消息 → mesh_models 回调 → led_output set → output_state (加锁)
按键 ISR → worker → led_output cycle → output_state (加锁)
主循环 33ms → led_output render → effect->render() → pixels[] → SPI → WS2812关键设计:所有控制来源(Mesh / 按键 / 未来扩展),最终都汇聚到 led_output 层的同一套 API,新增控制通道无需修改灯带逻辑。
1.3 Mesh 控制接口预留
led_output 层对外暴露以下接口——将来无论是 Mesh、按键、串口 AT 命令还是其他控制方式,全部通过同一套 API 操作灯带:
| 接口 | 功能 | 被谁调用 |
|---|---|---|
led_output_set_onoff(bool) | 开关 | Mesh OnOff 回调 |
led_output_set_lightness(uint16_t) | 亮度 (0~65535) | Mesh Lightness 回调 |
led_output_set_effect(uint8_t) | 切灯效 (0~N) | Mesh Vendor 回调、按键 worker |
led_output_cycle_effect() | 循环切灯效 | 按键 worker |
led_output_get_onoff() | 读开关 | Mesh OnOff GET 回调 |
led_output_get_lightness() | 读亮度 | Mesh Lightness GET 回调 |
led_output_get_effect_id() | 读灯效 ID | Mesh Vendor GET 回调 |
led_output_effect_count() | 灯效总数 | Mesh Vendor GET 回调 |
2 模块一:让灯带先亮起来
目标:上电后 WS2812 灯带全部点亮为白色,验证硬件链路(SPI → WS2812)正常。本模块不引入灯效框架,用最简单的方式先让灯带发光。
2.1 需求分析
| 编号 | 需求 | 说明 |
|---|---|---|
| R01 | WS2812 灯带能通过 SPI 输出像素数据 | 设备树配置灯带节点,Zephyr LED Strip 驱动负责 SPI 传输 |
| R02 | 灯带输出层:初始化设备 + 渲染 | led_output 最小实现——只负责把 120 颗像素全填白色 |
| R03 | main.c 初始化灯带并主循环渲染 | 上电即亮,33ms 一帧 |
为什么先不搭灯效框架? 先验证硬件链路(SPI 引脚、灯带接线、设备树、驱动加载)都没有问题,确认灯带能亮之后再在模块二引入框架重构。硬件有问题时,越少的代码越容易定位。
2.2 硬件设计
GPIO 引脚:
| GPIO | 功能 | 连接方式 |
|---|---|---|
| P0.12 | SPI1 SCK | PCB 走线(仅 nRF52840 驱动侧,不引到排针) |
| P0.13 | SPI1 MOSI | PCB 走线 → H2 排针 Pin2 → WS2812 灯带 DAT 引脚 |
灯带连接方式:
nRF52840 H2 排针 WS2812 灯带
P0.13 (SPI1 MOSI) ────────── Pin2 (DAT) ──── DIN
Pin1 (VCC) ──── 5V 电源
GND ─────────────── Pin3 (GND) ──── GND- 信号线直连:nRF52840 GPIO 输出 3.3V → WS2812。WS2812 @5V 时要求 VIH ≥ 3.5V,3.3V 处于临界值。短距离直连通常能工作,量产建议加 74HCT125 电平转换。
- 灯带推荐独立 5V 供电:120 颗全白峰值电流约 7.2A,不可从板载 LDO 取电。
- SPI MOSI 空闲时必须保持低电平(CPOL=0),否则 WS2812 无法识别 RESET 信号锁存数据。
SPI 驱动 WS2812 原理:1 个 WS2812 bit 由 1 个 SPI byte 模拟。6.4MHz 时 0x70 (3H+5L) 模拟 1-bit,0x40 (1H+7L) 模拟 0-bit。
2.3 BSP 配置
设备树:WS2812 灯带节点(在 mesh_led_strip.dts 中):
aliases {
led-strip = &led_strip;
};
&spi1 {
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-spi";
reg = <0>;
spi-max-frequency = <6400000>;
chain-length = <120>;
color-mapping = <LED_COLOR_ID_GREEN
LED_COLOR_ID_RED
LED_COLOR_ID_BLUE>;
bits-per-symbol = <8>;
spi-zero-frame = <0x40>;
spi-one-frame = <0x70>;
};
};pinctrl:SPI1 引脚复用(在 mesh_led_strip-pinctrl.dtsi 中):
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 12)>,
<NRF_PSEL(SPIM_MOSI, 0, 13)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 12)>,
<NRF_PSEL(SPIM_MOSI, 0, 13)>;
low-power-enable;
};
};Flash 分区表(在 mesh_led_strip.dts 中):
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>; #size-cells = <1>;
boot_partition: partition@0 { label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(48)>; };
slot0_partition: partition@c000 { label = "image-0"; reg = <0x0000c000 DT_SIZE_K(472)>; };
slot1_partition: partition@82000{ label = "image-1"; reg = <0x00082000 DT_SIZE_K(472)>; };
storage_partition: partition@f8000{label = "storage"; reg = <0x000f8000 DT_SIZE_K(32)>; };
};
};prj.conf(本阶段最小配置):
CONFIG_LED_STRIP=y
CONFIG_LOG=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=3072CMakeLists.txt(本阶段还不涉及 effects.ld,暂时不加):
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(led_strip)
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS src/*.c)
target_sources(app PRIVATE ${app_sources})
target_include_directories(app PRIVATE src)2.4 涉及的 NCS/Zephyr 组件
| 组件 | 头文件 | 作用 |
|---|---|---|
| LED Strip 驱动 | <zephyr/drivers/led_strip.h> | led_strip_update_rgb() 输出像素到 SPI |
| 设备树 API | <zephyr/device.h>, <zephyr/devicetree.h> | DEVICE_DT_GET(DT_ALIAS(led_strip)) 获取设备 |
| 日志 | <zephyr/logging/log.h> | RTT 输出调试信息 |
2.5 代码实现
2.5.1 led_output.c — 最简灯带输出层
led_output.h:
#ifndef LED_OUTPUT_H__
#define LED_OUTPUT_H__
int led_output_init(void);
int led_output_render_step(void);
#endif /* LED_OUTPUT_H__ */led_output.c:
#include "led/led_output.h"
#include <errno.h>
#include <string.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(led_output, LOG_LEVEL_INF);
#define STRIP_NODE DT_ALIAS(led_strip)
#define STRIP_NUM_PIXELS DT_PROP(STRIP_NODE, chain_length) // 120
static const struct device *const strip = DEVICE_DT_GET(STRIP_NODE);
static struct led_rgb pixels[STRIP_NUM_PIXELS];
int led_output_init(void)
{
if (!device_is_ready(strip)) {
LOG_ERR("LED strip device not ready");
return -ENODEV;
}
LOG_INF("LED strip ready: %s, %u pixels", strip->name, STRIP_NUM_PIXELS);
return 0;
}
int led_output_render_step(void)
{
// 最简单方式:全部填充白色,约 50% 亮度
struct led_rgb white = { .r = 0x80, .g = 0x80, .b = 0x80 };
for (size_t i = 0; i < STRIP_NUM_PIXELS; i++) {
pixels[i] = white;
}
return led_strip_update_rgb(strip, pixels, STRIP_NUM_PIXELS);
}本阶段是最简实现——没有任何效果框架,没有任何状态管理,120 颗灯珠全部硬编码填充白色。目的仅在于确认硬件链路正常。
2.5.2 main.c — 本阶段的入口
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include "led/led_output.h"
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting mesh led strip node");
if (led_output_init()) {
LOG_ERR("led_output_init failed");
return -1;
}
while (1) {
led_output_render_step();
k_sleep(K_MSEC(33)); // ~30fps
}
return 0;
}2.6 参考提示词
我要用 Nordic NCS v3.2.3 + nRF52840 做一个 WS2812 灯带项目,灯带 120 颗灯珠。
请帮我用最简单的方式让灯带先亮起来——不需要任何效果框架,只需要确认硬件链路正常。
【BSP 配置】
- 设备树:WS2812 over SPI1, SCK=P0.12, MOSI=P0.13, 6.4MHz
compatible="worldsemi,ws2812-spi", chain-length=120
color-mapping=GREEN RED BLUE, spi-zero-frame=0x40, spi-one-frame=0x70
- pinctrl:SPI1 的 default/sleep 两组
- prj.conf:CONFIG_LED_STRIP=y + RTT 日志
- CMakeLists.txt:GLOB_RECURSE src/*.c
【LED 输出层】
- led_output.c/h:最简实现
- init():device_is_ready 检查设备就绪
- render_step():把 120 颗像素全部填白色 (0x80, 0x80, 0x80) → 调用 led_strip_update_rgb()
- 不需要 Mutex,不需要状态管理,不需要效果框架——这些后面再加
【main.c】
- led_output_init() 后进入 while(1) 循环,每 33ms 调一次 render_step()
- 上电后 120 颗灯珠应该全部亮白色2.7 验证方案
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 编译项目 | 0 error 0 warning |
| 2 | 烧录到 nRF52840 板,插上 WS2812 灯带 | 120 颗灯珠全部亮白色 |
| 3 | RTT Viewer 查看日志 | 打印 LED strip ready: ws2812@0, 120 pixels |
| 4 | 拔掉灯带重新上电 | LED strip device not ready,程序退出(确认设备检查有效) |
| 5 | 用逻辑分析仪抓 P0.13 | 看到 6.4MHz SPI 数据波形 |
模块一验证通过后,硬件链路就确认没问题了。后续所有问题都是软件层面的。
3 模块二:搭建灯效框架 + 实现所有灯效
目标:在硬件已验证的基础上,引入灯效插件框架,实现 4 个灯效,并重构 led_output 使用框架。新增灯效只需新建一个
.c文件,无需改其他代码。
3.1 需求分析
| 编号 | 需求 | 说明 |
|---|---|---|
| R01 | 灯效抽象接口 effect_api.h | 统一的渲染函数签名 + 亮度缩放工具 + 自动注册宏 |
| R02 | 灯效注册表 effect_registry | 基于 Zephyr ITERABLE SECTION,编译期自动收集灯效 |
| R03 | 链接脚本 effects.ld + CMakeLists 注入 | 让 linker 生成注册表起止符号,运行时可遍历 |
| R04 | 重构 led_output 对接灯效框架 | 改为从注册表取灯效 → 调用 render → 维护帧计数 |
| R05 | solid (id=0):全带白色常亮 | 标准参考实现 |
| R06 | flow (id=1):单绿点向后流动 | frame % count 决定亮点位置 |
| R07 | breathe (id=2):全带蓝色呼吸 | 三角波 64 帧周期 |
| R08 | rainbow (id=3):彩虹渐变流动 | 色轮函数,每像素位置+帧号算颜色 |
| R09 | 实现 set_effect / cycle_effect | 用于手动切换,为按键和 Mesh 做准备 |
3.2 涉及硬件
无新增硬件——纯软件重构。灯带硬件已在模块一验证通过。
3.3 BSP 配置补充
本阶段需要在 CMakeLists.txt 中增加一行——注入灯效注册表链接脚本:
zephyr_linker_sources(SECTIONS src/effects/effects.ld)这一行绝对不能忘:不加的话编译不会报错,但
effect_registry_count()返回 0,所有灯效都无法使用。
完整 CMakeLists.txt 变为:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(led_strip)
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS src/*.c)
target_sources(app PRIVATE ${app_sources})
target_include_directories(app PRIVATE src)
zephyr_linker_sources(SECTIONS src/effects/effects.ld)prj.conf 不变——本阶段无需新 Kconfig。
3.4 涉及的 NCS/Zephyr 组件
| 组件 | 头文件 | 作用 |
|---|---|---|
| Iterable Sections | <zephyr/sys/iterable_sections.h> | STRUCT_SECTION_ITERABLE / STRUCT_SECTION_COUNT / STRUCT_SECTION_FOREACH |
| Linker Sections | <zephyr/linker/iterable_sections.h> | ITERABLE_SECTION_ROM 声明链接段 |
| LED Strip | <zephyr/drivers/led_strip.h> | struct led_rgb 类型 |
| Mutex | <zephyr/kernel.h> — k_mutex | 为后续 Mesh 并发预留 |
ITERABLE SECTION 原理:
LED_EFFECT_REGISTER(var, ...)→STRUCT_SECTION_ITERABLE→ 变量放入.led_effect_desc专有段- 链接时
effects.ld把所有目标文件的该段合并为连续数组,生成起止符号 - 运行时
STRUCT_SECTION_FOREACH遍历起止符号之间的所有条目
3.5 代码实现
3.5.1 effect_api.h — 灯效接口定义
#ifndef EFFECT_API_H__
#define EFFECT_API_H__
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/sys/iterable_sections.h>
// 灯效输入上下文
struct led_effect_context {
bool power_on;
uint16_t lightness;
uint32_t frame;
};
// 灯效描述符
struct led_effect_desc {
uint8_t effect_id;
const char *name;
void (*render)(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count);
};
// 亮度缩放
static inline uint8_t led_effect_scale_u8(uint16_t lightness, uint8_t value)
{
return (uint8_t)(((uint32_t)value * lightness) / UINT16_MAX);
}
static inline struct led_rgb led_effect_rgb(uint16_t lightness,
uint8_t r, uint8_t g, uint8_t b)
{
struct led_rgb px = {
.r = led_effect_scale_u8(lightness, r),
.g = led_effect_scale_u8(lightness, g),
.b = led_effect_scale_u8(lightness, b),
};
return px;
}
// 灯效注册宏
#define LED_EFFECT_REGISTER(_var_name, _effect_id, _effect_name, _render_fn) \
const STRUCT_SECTION_ITERABLE(led_effect_desc, _var_name) = { \
.effect_id = (_effect_id), \
.name = (_effect_name), \
.render = (_render_fn), \
}
#endif3.5.2 effects.ld — 链接段声明
#include <zephyr/linker/iterable_sections.h>
ITERABLE_SECTION_ROM(led_effect_desc, Z_LINK_ITERABLE_SUBALIGN)3.5.3 effect_registry — 灯效注册表
// effect_registry.c
#include "effects/effect_registry.h"
#include <zephyr/sys/iterable_sections.h>
size_t effect_registry_count(void)
{
size_t count;
STRUCT_SECTION_COUNT(led_effect_desc, &count);
return count;
}
const struct led_effect_desc *effect_registry_get_by_id(uint8_t effect_id)
{
STRUCT_SECTION_FOREACH(led_effect_desc, effect) {
if (effect->effect_id == effect_id) return effect;
}
return NULL;
}
const struct led_effect_desc *effect_registry_get_by_index(size_t index)
{
size_t i = 0;
STRUCT_SECTION_FOREACH(led_effect_desc, effect) {
if (i == index) return effect;
i++;
}
return NULL;
}
const struct led_effect_desc *effect_registry_first(void)
{
return effect_registry_get_by_index(0);
}3.5.4 重构 led_output — 对接灯效框架
核心改动:把模块一的硬编码白色填充,替换为"从注册表取灯效 → 调用 render"。
// led_output.c(重构版,关键代码摘录)
struct led_output_state {
bool onoff;
uint16_t lightness;
uint16_t last_lightness; // 为 Mesh 预留:关灯记忆
uint8_t effect_id;
uint32_t frame;
};
static struct k_mutex output_lock;
static struct led_output_state output_state;
static const struct device *const strip = DEVICE_DT_GET(DT_ALIAS(led_strip));
static struct led_rgb pixels[120];
static void state_snapshot(struct led_output_state *snapshot)
{
k_mutex_lock(&output_lock, K_FOREVER);
*snapshot = output_state;
k_mutex_unlock(&output_lock);
}
int led_output_init(void)
{
const struct led_effect_desc *effect = effect_registry_first();
if (!device_is_ready(strip)) { LOG_ERR("LED strip not ready"); return -ENODEV; }
if (!effect) { LOG_ERR("No effects registered"); return -ENOENT; }
k_mutex_init(&output_lock);
output_state = (struct led_output_state){
.onoff = true, .lightness = 0x8000,
.last_lightness = 0x8000, .effect_id = effect->effect_id, .frame = 0U
};
LOG_INF("LED strip ready, effects=%u, default=%s",
effect_registry_count(), effect->name);
return 0;
}
int led_output_render_step(void)
{
struct led_output_state snap;
state_snapshot(&snap);
memset(pixels, 0, sizeof(pixels));
if (snap.onoff && snap.lightness > 0U) {
const struct led_effect_desc *effect = effect_registry_get_by_id(snap.effect_id);
if (!effect) return -ENOENT;
struct led_effect_context ctx = {
.power_on = snap.onoff, .lightness = snap.lightness, .frame = snap.frame
};
effect->render(&ctx, pixels, 120);
}
int err = led_strip_update_rgb(strip, pixels, 120);
if (err) return err;
k_mutex_lock(&output_lock, K_FOREVER);
if (output_state.onoff && output_state.lightness > 0U) output_state.frame++;
k_mutex_unlock(&output_lock);
return 0;
}
// === 灯效切换接口 ===
int led_output_set_effect(uint8_t effect_id)
{
if (effect_registry_get_by_id(effect_id) == NULL) return -ENOENT;
k_mutex_lock(&output_lock, K_FOREVER);
output_state.effect_id = effect_id;
output_state.frame = 0U;
k_mutex_unlock(&output_lock);
return 0;
}
int led_output_cycle_effect(void)
{
size_t count = effect_registry_count();
if (count == 0U) return -ENOENT;
uint8_t current = led_output_get_effect_id();
size_t idx = 0;
for (size_t i = 0; i < count; i++) {
const struct led_effect_desc *e = effect_registry_get_by_index(i);
if (e && e->effect_id == current) { idx = i; break; }
}
const struct led_effect_desc *next = effect_registry_get_by_index((idx + 1) % count);
if (!next) return -ENOENT;
return led_output_set_effect(next->effect_id);
}
// === 状态读取接口(为 Mesh 预留)===
bool led_output_get_onoff(void) { /* ... */ }
uint16_t led_output_get_lightness(void) { /* ... */ }
uint8_t led_output_get_effect_id(void) { /* ... */ }
uint8_t led_output_effect_count(void) { return (uint8_t)effect_registry_count(); }
const char *led_output_effect_name(uint8_t id) { /* ... */ }led_output.h 对外接口:
// 初始化与渲染
int led_output_init(void);
int led_output_render_step(void);
// 控制接口(Mesh 预留)
void led_output_set_onoff(bool onoff);
void led_output_set_lightness(uint16_t lightness);
int led_output_set_effect(uint8_t effect_id);
int led_output_cycle_effect(void);
// 查询接口(Mesh 预留)
bool led_output_get_onoff(void);
uint16_t led_output_get_lightness(void);
uint8_t led_output_get_effect_id(void);
uint8_t led_output_effect_count(void);
const char *led_output_effect_name(uint8_t effect_id);3.5.5 effect_solid.c — 常亮灯效 (id=0)
#include "effects/effect_api.h"
static void render_solid(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count)
{
struct led_rgb px = led_effect_rgb(ctx->lightness, 0xff, 0xff, 0xff);
for (size_t i = 0; i < count; i++) pixels[i] = px;
}
LED_EFFECT_REGISTER(effect_solid, 0, "solid", render_solid);3.5.6 effect_flow.c — 流动灯效 (id=1)
#include <string.h>
#include "effects/effect_api.h"
static void render_flow(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count)
{
size_t index = ctx->frame % count;
memset(pixels, 0, count * sizeof(*pixels));
pixels[index] = led_effect_rgb(ctx->lightness, 0x00, 0xff, 0x30);
}
LED_EFFECT_REGISTER(effect_flow, 1, "flow", render_flow);3.5.7 effect_breathe.c — 呼吸灯效 (id=2)
#include "effects/effect_api.h"
static uint8_t breathe_level(uint32_t frame)
{
uint32_t phase = frame % 64U;
uint32_t value = (phase < 32U) ? phase : (63U - phase);
return (uint8_t)(value * 8U);
}
static void render_breathe(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count)
{
uint16_t local = ((uint32_t)ctx->lightness * breathe_level(ctx->frame)) / UINT8_MAX;
struct led_rgb px = led_effect_rgb(local, 0x10, 0x40, 0xff);
for (size_t i = 0; i < count; i++) pixels[i] = px;
}
LED_EFFECT_REGISTER(effect_breathe, 2, "breathe", render_breathe);3.5.8 effect_rainbow.c — 彩虹灯效 (id=3)
#include "effects/effect_api.h"
static struct led_rgb wheel(uint16_t lightness, uint8_t pos)
{
if (pos < 85U)
return led_effect_rgb(lightness, 255U - pos * 3U, pos * 3U, 0U);
pos -= 85U;
if (pos < 85U)
return led_effect_rgb(lightness, 0U, 255U - pos * 3U, pos * 3U);
pos -= 85U;
return led_effect_rgb(lightness, pos * 3U, 0U, 255U - pos * 3U);
}
static void render_rainbow(const struct led_effect_context *ctx,
struct led_rgb *pixels, size_t count)
{
for (size_t i = 0; i < count; i++) {
uint8_t pos = (uint8_t)((i * 256U / count) + ctx->frame);
pixels[i] = wheel(ctx->lightness, pos);
}
}
LED_EFFECT_REGISTER(effect_rainbow, 3, "rainbow", render_rainbow);3.5.9 main.c 变化
本阶段 main.c 不需要修改——灯效框架的注册是编译期自动的,led_output 重构后对外接口不变。
但为了方便验证,可临时加入循环切换:
// 临时验证:每 3 秒自动切一次灯效
int main(void)
{
led_output_init();
int tick = 0;
while (1) {
led_output_render_step();
k_sleep(K_MSEC(33));
if (++tick >= 90) { tick = 0; led_output_cycle_effect(); }
}
return 0;
}3.6 参考提示词
我的灯带项目模块一已经完成——灯带能亮了,led_output 目前是硬编码全白填充。
现在要引入灯效插件框架,重构 led_output,并实现 4 个灯效。
【请帮我做三件事】
=== 第一件:灯效框架 ===
1. effect_api.h:struct led_effect_context/led_effect_desc、led_effect_rgb()、
LED_EFFECT_REGISTER 宏 (STRUCT_SECTION_ITERABLE)
2. effects.ld:ITERABLE_SECTION_ROM(led_effect_desc, 4)
3. effect_registry.c/h:count/get_by_id/get_by_index/first
4. CMakeLists.txt 加一行:zephyr_linker_sources(SECTIONS src/effects/effects.ld)
=== 第二件:重构 led_output ===
- 引入 struct led_output_state { onoff, lightness, last_lightness, effect_id, frame }
- k_mutex 保护(为 Mesh 并发预留)
- init() 从 effect_registry_first 取默认灯效,50% 亮度开灯
- render_step():快照 → 查灯效 → render(ctx, pixels, 120) → 输出 → frame++
- 实现 set_effect(id) / cycle_effect()
- 实现 get_onoff/get_lightness/get_effect_id(Mesh GET 预留)
=== 第三件:4 个灯效 ===
- effect_solid.c (id=0):全带白色常亮
- effect_flow.c (id=1):单点绿色,位置 frame%count
- effect_breathe.c (id=2):64帧三角波,蓝色呼吸
- effect_rainbow.c (id=3):色轮 wheel(lightness,pos),pos=(i*256/count+frame)%256
- 每个文件末尾 LED_EFFECT_REGISTER(effect_xxx, id, "name", render_xxx)
【关键点】
- effects.ld 和 CMakeLists 中的 zephyr_linker_sources 绝不能忘
- 灯效纯整数运算,不依赖浮点
- 每个灯效独立 .c 文件,不改其他代码3.7 验证方案
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 编译 | 0 error,RTT 打印 effects=4, default=solid |
| 2 | 上电看灯带 | 亮白色(视觉与模块一相同,但底层已是框架) |
| 3 | 临时加 cycle_effect() 每 3 秒切一次 | solid→flow→breathe→rainbow→solid |
| 4 | 验证 flow | 单点绿光向后移动 |
| 5 | 验证 breathe | 蓝色周期呼吸 |
| 6 | 验证 rainbow | 彩虹色流动 |
| 7 | 验证"新增即注册" | 新建 effect_test.c → 编译后 count 自动 +1 |
| 8 | 测试无效灯效 | set_effect(0xFF) 返回 -ENOENT |
| 9 | 删除临时验证代码 | 恢复 while(1) render_step |
模块二完成后,灯效框架和 4 个灯效就位,
cycle_effect/set_effect已可调用——下一个模块加入按键或 Mesh 控制即可。
4 模块三:加上本地按键
目标:按下 KEY2 按键,循环切换到下一个灯效。
4.1 需求分析
| 编号 | 需求 | 说明 |
|---|---|---|
| R01 | 按键 sw1 (KEY2, P1.15) 短按循环切换灯效 | solid→flow→breathe→rainbow→solid |
| R02 | ISR 做 200ms 软件去抖 | 防止按键抖动误触发多次 |
| R03 | ISR 提交 worker 到系统 workqueue | ISR 中不调 cycle_effect(有 mutex 锁) |
| R04 | 切换后日志输出当前灯效名称 | 方便 RTT 确认按键有效 |
4.2 硬件设计
GPIO 引脚:
| GPIO | 按键丝印 | 设备树别名 | 功能 |
|---|---|---|---|
| P0.18 | RESET (SW4) | — | 系统复位(硬件复位,不经 GPIO 中断) |
| P1.00 | KEY1 (SW5) | sw0 | 预留,固件未使用 |
| P1.15 | KEY2 (SW6) | sw1 | 灯效切换(本模块使用) |
按键电路原理(三个按键电路完全相同):
3.3V ──┬── R (10kΩ, 厚声 0603WAF1002T5E) ──┬── GPIO 引脚 (nRF52840)
│ │
│ ├── C (100nF, X7R) ── GND
│ │
└── 按键 (TS3425BA) ────────────────────┘
│
GND- 默认状态:按键未按下,GPIO 通过 10kΩ 上拉到 3.3V,读取为高电平(逻辑 1)
- 按下状态:按键导通,GPIO 被拉到 GND,读取为低电平(逻辑 0)
- RC 滤波:10kΩ × 100nF = τ=1ms,抑制按键机械抖动。配合固件 200ms 软件去抖实现可靠检测
- 中断配置:
GPIO_INT_EDGE_TO_ACTIVE(下降沿触发),GPIO_ACTIVE_LOW(低电平 = 按下) - SW4 (RESET) 特殊说明:P0.18 是 nRF52840 的 nRESET 硬件复位引脚,按下后芯片直接复位,不走 GPIO 中断。这与 KEY1/KEY2 的固件中断方式不同。
连接方式:按键是 SMD 4-Pin 立贴封装,直接焊在 PCB 上,无需外部接线。用户操作时按压按键帽即可。
4.3 BSP 配置补充
设备树:按键节点(在 mesh_led_strip.dts 中):
aliases {
sw0 = &button0;
sw1 = &button1; // 代码通过 DT_ALIAS(sw1) 获取
};
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 4 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "KEY1";
};
button1: button_1 {
gpios = <&gpio0 15 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "KEY2";
};
};
&gpiote {
status = "okay"; // 按键中断依赖 GPIOTE
};
&gpio0 {
status = "okay";
};pinctrl 补充(已在模块一中配置了 SPI1,UART0 可选):
uart0_default: uart0_default {
group1 { psels = <NRF_PSEL(UART_TX, 0, 6)>; };
group2 { psels = <NRF_PSEL(UART_RX, 0, 8)>; bias-pull-up; };
};
uart0_sleep: uart0_sleep {
group1 { psels = <NRF_PSEL(UART_TX, 0, 6)>, <NRF_PSEL(UART_RX, 0, 8)>; low-power-enable; };
};4.4 涉及的 NCS/Zephyr 组件
| 组件 | 头文件 | 作用 |
|---|---|---|
| GPIO | <zephyr/drivers/gpio.h> | gpio_pin_configure_dt, gpio_add_callback |
| Kernel Work | <zephyr/kernel.h> | k_work_init, k_work_submit |
| 系统时钟 | <zephyr/kernel.h> — k_uptime_get() | 软件去抖时间戳 |
4.5 代码实现
main.c 中新增按键相关代码:
#include <zephyr/drivers/gpio.h>
#define SW1_NODE DT_ALIAS(sw1)
#define BUTTON_DEBOUNCE_MS 200
static const struct gpio_dt_spec sw1 = GPIO_DT_SPEC_GET(SW1_NODE, gpios);
static struct gpio_callback sw1_cb_data;
static struct k_work sw1_work;
static int64_t sw1_last_press_ms;
// Worker:系统 workqueue 上下文中执行
static void sw1_pressed_worker(struct k_work *work)
{
ARG_UNUSED(work);
if (led_output_cycle_effect() == 0) {
LOG_INF("Button: effect id=%u name=%s",
led_output_get_effect_id(),
led_output_effect_name(led_output_get_effect_id()));
}
}
// ISR:中断上下文,只做去抖 + 提交 work
static void sw1_pressed_isr(const struct device *dev,
struct gpio_callback *cb, uint32_t pins)
{
int64_t now = k_uptime_get();
if ((now - sw1_last_press_ms) < BUTTON_DEBOUNCE_MS) return; // 去抖
sw1_last_press_ms = now;
k_work_submit(&sw1_work); // 提交给系统 workqueue,不在此处等锁
}
static int sw1_init(void)
{
if (!gpio_is_ready_dt(&sw1)) return -ENODEV;
gpio_pin_configure_dt(&sw1, GPIO_INPUT);
gpio_pin_interrupt_configure_dt(&sw1, GPIO_INT_EDGE_TO_ACTIVE);
gpio_init_callback(&sw1_cb_data, sw1_pressed_isr, BIT(sw1.pin));
gpio_add_callback(sw1.port, &sw1_cb_data);
k_work_init(&sw1_work, sw1_pressed_worker);
sw1_last_press_ms = 0;
return 0;
}main.c 完整版:
int main(void)
{
LOG_INF("Starting mesh led strip node");
led_output_init(); // 模块一:灯带
sw1_init(); // 模块三:按键(本模块新增)
while (1) {
led_output_render_step();
k_sleep(K_MSEC(33));
}
return 0;
}4.6 参考提示词
我的灯带项目已经实现了灯带点亮和 4 个内置灯效。现在要加一个本地按键,
按一次切到下一个灯效。
【硬件信息】
- KEY2 (sw1): GPIO P1.15,上拉+低电平有效
- 设备树别名: sw1 = &button1
【按键实现要求】
1. sw1_init(): GPIO 输入 + 边缘触发中断 + 注册回调 + 初始化 work
2. ISR 用 k_uptime_get() 做 200ms 软件去抖
3. ISR 只提交 k_work_submit,不做业务逻辑
-- 因为 led_output_cycle_effect() 里面有 k_mutex 锁,ISR 不能等锁!
4. worker 中调用 led_output_cycle_effect(),然后 LOG_INF 打印当前灯效 id 和名称
【设备树】
- aliases: sw1 = &button1
- button1: gpios = <&gpio0 15 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>
- &gpiote { status = "okay" } ← 按键中断依赖
- &gpio0 { status = "okay" }
【main.c】
初始化顺序: led_output_init() → sw1_init() → 渲染循环4.7 验证方案
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 上电,RTT 查看日志 | SW1 button ready for local effect switching |
| 2 | 短按 KEY2 × 1 次 | 灯带切到 flow,RTT: Button: effect id=1 name=flow |
| 3 | 短按 KEY2 × 3 次 | flow→breathe→rainbow→solid |
| 4 | 快速连按 5 次 | 只有 1~2 次生效(200ms 去抖) |
| 5 | 长按不放 | 只触发 1 次(边缘触发,松手不触发) |
5 模块四:加上 BLE Mesh 远程控制
目标:手机 nRF Mesh App 通过 BLE Mesh 协议开关灯带、调节亮度、切换灯效。
5.1 需求分析
| 编号 | 需求 | 说明 |
|---|---|---|
| R01 | BLE 协议栈初始化,蓝牙广播 | UUID 生成 → bt_enable() 异步启动 |
| R02 | Mesh 配网 (Provisioning) | PB-ADV + PB-GATT 双通道,配网完成/重置日志输出 |
| R03 | 标准模型 — Generic OnOff Server | SET/GET 回调 → led_output_set_onoff/get_onoff() |
| R04 | 标准模型 — Light Lightness Server | SET/GET 回调 → led_output_set_lightness/get_lightness() |
| R05 | Vendor 模型 — Effect Server | GET/SET/SET_UNACK 操作码 → led_output_set_effect/get_effect_id() |
| R06 | Mesh 模型初始状态与 LED 层同步 | mesh_models_init 时从 led_output 读取当前状态 |
5.2 硬件设计
GPIO 引脚:蓝牙模块集成在 nRF52840 片内,通过 ANT 引脚连接外部天线匹配网络。
天线连接方式:
nRF52840 ANT (H23)
│
├── L2 (4.7nH, 串联, muRata LQG15HS) ──┬── C214 (0.8pF, 串联, muRata GJM) ── SMD 陶瓷天线 ANT1 (ANT_CA-C03)
│ │
└── C215 (0.8pF, 并联, muRata GJM) ─────┴── GND- π 型匹配网络:L2 + C214 + C215 将 nRF52840 ANT 引脚的非标准阻抗匹配到天线 50Ω。L/C 值为参考值,需通过矢量网络分析仪实测调试确定最终参数。
- 天线:SMD 陶瓷天线 ANT_CA-C03,2.4~2.5GHz,50Ω 阻抗。天线周围 PCB 区域净空,下方无地平面。
- PCB 走线:ANT 引脚到天线之间的微带线按 50Ω 特性阻抗设计。
- 无需用户接线:蓝牙天线在 PCB 上集成了,用户看不到天线引脚,只需通过手机 App 无线连接。
电路拓扑:蓝牙部分对外无连接器——所有通信通过 2.4GHz 无线完成。调试/配网通过 USB-C (CH340N UART) 查看 RTT 日志。
5.3 BSP 配置补充
本阶段新增的 prj.conf 配置:
# BLE 协议栈
CONFIG_BT=y
CONFIG_BT_DEVICE_NAME="mesh_led_strip"
CONFIG_BT_L2CAP_TX_BUF_COUNT=8
CONFIG_BT_OBSERVER=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_RX_STACK_SIZE=2700
# BLE Mesh
CONFIG_BT_MESH=y
CONFIG_BT_MESH_RELAY=y
CONFIG_BT_MESH_FRIEND=y
CONFIG_BT_MESH_TX_SEG_MAX=10
CONFIG_BT_MESH_PB_GATT=y
CONFIG_BT_MESH_GATT_PROXY=y
CONFIG_BT_MESH_PROXY_USE_DEVICE_NAME=y
# 标准 Mesh 模型
CONFIG_BT_MESH_ONOFF_SRV=y
CONFIG_BT_MESH_LIGHTNESS_SRV=yMesh 持久化预留(Settings/NVS 基础已就绪,开关/亮度/灯效的存储尚未接入):
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_NVS_LOOKUP_CACHE=y
CONFIG_SETTINGS_NVS_NAME_CACHE=y
CONFIG_HWINFO=y
CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE=0x8000
CONFIG_SOC_FLASH_NRF_PARTIAL_ERASE=y
CONFIG_BT_SETTINGS=y5.4 涉及的 NCS/Zephyr 组件
| 组件 | 头文件/路径 | 作用 |
|---|---|---|
| Bluetooth | <zephyr/bluetooth/bluetooth.h> | bt_enable() 异步启动协议栈 |
| Mesh 核心 | <zephyr/bluetooth/mesh.h> | bt_mesh_init(), bt_mesh_prov_enable() |
| Mesh Access | <zephyr/bluetooth/mesh/access.h> | 消息收发 |
| OnOff Server | <bluetooth/mesh/gen_onoff_srv.h> | BT_MESH_ONOFF_SRV_INIT + handlers |
| Lightness Server | <bluetooth/mesh/lightness_srv.h> | BT_MESH_LIGHTNESS_SRV_INIT + handlers |
| Models 工具 | <bluetooth/mesh/models.h> | BT_MESH_MODEL_OP_3 — Vendor 操作码生成 |
| 硬件 ID | <zephyr/drivers/hwinfo.h> | hwinfo_get_device_id() 生成设备 UUID |
| Settings | <zephyr/settings/settings.h> | settings_load() 持久化 |
官方示例参考:nrf/samples/bluetooth/mesh/light(本项目 mesh_models.c 的核心参考)
5.5 代码实现
5.5.1 mesh_app.c — BLE + Mesh 协议栈初始化
#include "mesh/mesh_app.h"
#include <string.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include "mesh/mesh_models.h"
LOG_MODULE_REGISTER(mesh_app, LOG_LEVEL_INF);
static uint8_t dev_uuid[16];
// 生成设备 UUID:优先硬件 ID,失败用固定前缀回退
static void uuid_init(void)
{
ssize_t id_len = hwinfo_get_device_id(dev_uuid, sizeof(dev_uuid));
if (id_len <= 0) {
memset(dev_uuid, 0, sizeof(dev_uuid));
dev_uuid[0] = 0x4d; dev_uuid[1] = 0x4c;
dev_uuid[2] = 0x53; dev_uuid[3] = 0x01;
id_len = 4;
}
for (size_t i = id_len; i < sizeof(dev_uuid); i++)
dev_uuid[i] = dev_uuid[i % id_len] ^ 0xff;
// RFC-4122: Version 4, Variant 1
dev_uuid[6] = (dev_uuid[6] & 0x0f) | 0x40;
dev_uuid[8] = (dev_uuid[8] & 0x3f) | 0x80;
}
static void prov_complete(uint16_t net_idx, uint16_t addr)
{
LOG_INF("Provisioned: net_idx=0x%04x addr=0x%04x", net_idx, addr);
}
static void prov_reset(void)
{
LOG_INF("Mesh reset, enabling provisioning again");
bt_mesh_prov_enable(BT_MESH_PROV_ADV | BT_MESH_PROV_GATT);
}
static const struct bt_mesh_prov prov = {
.uuid = dev_uuid, .complete = prov_complete, .reset = prov_reset,
};
static void bt_ready(int err)
{
if (err) { LOG_ERR("Bluetooth init failed: %d", err); return; }
LOG_INF("Bluetooth initialized");
err = bt_mesh_init(&prov, mesh_models_init());
if (err) { LOG_ERR("Mesh init failed: %d", err); return; }
if (IS_ENABLED(CONFIG_SETTINGS)) settings_load();
bt_mesh_prov_enable(BT_MESH_PROV_ADV | BT_MESH_PROV_GATT);
LOG_INF("Mesh initialized");
}
int mesh_app_init(void)
{
uuid_init();
return bt_enable(bt_ready); // 异步:蓝牙就绪后自动调用 bt_ready
}5.5.2 mesh_models.c — 模型定义与回调
// 关键代码摘录,完整文件见源码
#define LED_EFFECT_COMPANY_ID CONFIG_BT_COMPANY_ID // 0x0059
#define LED_EFFECT_MODEL_ID 0x0001
#define LED_EFFECT_OP_GET BT_MESH_MODEL_OP_3(0x01, LED_EFFECT_COMPANY_ID)
#define LED_EFFECT_OP_SET BT_MESH_MODEL_OP_3(0x02, LED_EFFECT_COMPANY_ID)
#define LED_EFFECT_OP_SET_UNACK BT_MESH_MODEL_OP_3(0x03, LED_EFFECT_COMPANY_ID)
#define LED_EFFECT_OP_STATUS BT_MESH_MODEL_OP_3(0x04, LED_EFFECT_COMPANY_ID)
// OnOff Set 回调
static void onoff_set(struct bt_mesh_onoff_srv *srv, struct bt_mesh_msg_ctx *ctx,
const struct bt_mesh_onoff_set *set, struct bt_mesh_onoff_status *rsp)
{
led_output_set_onoff(set->on_off);
rsp->present_on_off = led_output_get_onoff();
rsp->target_on_off = rsp->present_on_off;
rsp->remaining_time = 0U;
}
// Lightness Set 回调
static void lightness_set(struct bt_mesh_lightness_srv *srv, struct bt_mesh_msg_ctx *ctx,
const struct bt_mesh_lightness_set *set,
struct bt_mesh_lightness_status *rsp)
{
led_output_set_lightness(set->lvl);
rsp->current = led_output_get_lightness();
rsp->target = rsp->current;
rsp->remaining_time = 0U;
}
// Vendor Effect SET 公共逻辑
static int effect_apply_set(const struct bt_mesh_model *model,
struct bt_mesh_msg_ctx *ctx,
struct net_buf_simple *buf, bool ack)
{
struct led_effect_srv *srv = model->rt->user_data;
uint8_t effect_id = net_buf_simple_pull_u8(buf);
uint8_t tid = net_buf_simple_pull_u8(buf); // 当前未使用
if (led_output_set_effect(effect_id)) {
LOG_WRN("Unsupported effect id %u", effect_id);
return -ENOENT;
}
LOG_INF("Effect changed to id=%u name=%s", effect_id,
led_output_effect_name(effect_id));
if (ack) return effect_status_send(srv, ctx); // 回复 STATUS
return 0;
}
// Element 组合
static struct bt_mesh_elem elements[] = {
BT_MESH_ELEM(1,
BT_MESH_MODEL_LIST(
BT_MESH_MODEL_CFG_SRV, // 强制
BT_MESH_MODEL_HEALTH_SRV(&health_srv, &health_pub),
BT_MESH_MODEL_ONOFF_SRV(&onoff_srv),
BT_MESH_MODEL_LIGHTNESS_SRV(&lightness_srv)),
BT_MESH_MODEL_LIST(
BT_MESH_MODEL_LED_EFFECT_SRV(&effect_srv))), // Vendor
};
// 初始状态同步
const struct bt_mesh_comp *mesh_models_init(void)
{
lightness_srv.range.min = 1U;
lightness_srv.range.max = UINT16_MAX;
lightness_srv.default_light = led_output_get_lightness();
lightness_srv.transient.last = lightness_srv.default_light;
lightness_srv.transient.is_on = led_output_get_onoff();
return ∁
}Mesh 回调 → LED 层调用链:
App 发送 SET (lightness=0x8000)
→ BLE 协议栈收到
→ lightness_set() 回调
→ led_output_set_lightness(0x8000) ← 调用 LED 层接口
→ 填充 rsp (remaining_time=0, 立即生效)
→ 协议栈自动回复 STATUS5.6 main.c 变化
本阶段在 main() 中增加一行 mesh_app_init():
int main(void)
{
LOG_INF("Starting mesh led strip node");
led_output_init(); // 模块一:灯带
sw1_init(); // 模块三:按键
mesh_app_init(); // 模块四:Mesh(本阶段新增)
while (1) {
led_output_render_step();
k_sleep(K_MSEC(33));
}
return 0;
}5.7 参考提示词
我的灯带项目已经实现了灯带点亮、4 个灯效和本地按键切换。
现在需要加上 BLE Mesh 控制——通过手机 nRF Mesh App 控制开关、亮度、灯效。
【硬件信息】
- MCU: nRF52840
- 设备名称: "mesh_led_strip"
- Company ID: 0x0059
- 配网: PB-ADV + PB-GATT
- 灯带接口: led_output.h (set_onoff/set_lightness/set_effect/get_xxx 等)
【请帮我实现两个文件: mesh_app.c + mesh_models.c】
=== mesh_app.c ===
1. uuid_init(): hwinfo_get_device_id() 读硬件ID → 失败用 {0x4d,0x4c,0x53,0x01} 回退 → 16字节补齐 → RFC-4122修正
2. prov 结构体: .complete 打印 net_idx/addr, .reset 重新使能配网
3. bt_ready(): bt_mesh_init() → settings_load() → bt_mesh_prov_enable(PB_ADV|PB_GATT)
4. mesh_app_init(): uuid_init() → bt_enable(bt_ready)
=== mesh_models.c ===
5. Vendor 操作码 (Company=0x0059, Model=0x0001):
GET=0xC10059(0字节→回复[id][count]),
SET=0xC20059(2字节[id][tid]→切灯效→回复STATUS),
SET_UNACK=0xC30059(2字节→切灯效→不回复)
6. OnOff Set/Get → led_output_set_onoff/get_onoff
7. Lightness Set/Get → led_output_set_lightness/get_lightness
8. Vendor Get → 回复 [effect_id][count]
9. Vendor Set → net_buf_simple_pull_u8 取 id+tid → led_output_set_effect → 回复
10. Element: CFG_SRV+HEALTH_SRV+ONOFF_SRV+LIGHTNESS_SRV (主列表) + EFFECT_SRV (Vendor列表)
11. mesh_models_init(): 同步 led_output 当前状态到 lightness_srv
需要升级 prj.conf 加 CONFIG_BT=y, CONFIG_BT_MESH=y, CONFIG_BT_MESH_ONOFF_SRV=y, CONFIG_BT_MESH_LIGHTNESS_SRV=y 等配置。5.8 验证方案
| 阶段 | 步骤 | 操作 | 预期结果 |
|---|---|---|---|
| A. 环境 | 1 | 编译烧录,RTT 查看日志 | Bluetooth initialized → Mesh initialized |
| 2 | 打开 nRF Mesh App 扫描 | 发现设备 mesh_led_strip | |
| B. 配网 | 3 | App 点击 Provision | 配网成功,RTT: Provisioned: net_idx=0x0000 addr=0xXXXX |
| 4 | 在 App 中绑定 AppKey 到 Element 1 | 模型列表出现 OnOff Server + Lightness Server | |
| C. 开关 | 5 | Generic OnOff → 点击 ON/OFF | 灯带亮/灭 |
| 6 | Generic OnOff → Read | 返回当前开关状态 | |
| D. 亮度 | 7 | Lightness 滑块拖到 25% | 灯带变暗 |
| 8 | Lightness 滑块拖到 100% | 灯带最亮 | |
| 9 | Lightness = 0 | 灯灭,get_onoff() 返回 false | |
| 10 | Lightness Read | 返回当前亮度值 | |
| E. 灯效 | 11 | Vendor GET (opcode 0xC10059, 空参数) | 回复 [id][count] |
| 12 | Vendor SET (opcode 0xC20059, 00 01) | 灯带切 solid,回复 STATUS | |
| 13 | SET (opcode 0xC20059, 03 04) | 灯带切 rainbow,RTT: Effect changed to id=3 name=rainbow | |
| 14 | SET 无效 id (0xFF) | RTT: Unsupported effect id 255,灯效不变 | |
| 15 | SET_UNACK (opcode 0xC30059, 01 02) | 灯带切 flow,无回复 |
6 构建与最终验证
6.1 构建命令
powershell -ExecutionPolicy Bypass -File `
"C:\Users\skiin\.codex\skills\ncs-fresh-build\scripts\invoke_ncs_fresh_build.ps1" `
-AppDir "C:\projects\led_strip" `
-SdkVersion "3.2.3" `
-Board "mesh_led_strip/nrf52840" `
-BoardRoot "C:/projects/led_strip" `
-Snippet "rtt-console" `
-NoSysbuild `
-ForceFresh6.2 完整功能验证清单
| 编号 | 测试项 | 操作 | 预期结果 |
|---|---|---|---|
| T-01 | 上电自检 | 上电看 RTT | 依次打印 LED strip ready → Bluetooth initialized → Mesh initialized → SW1 button ready,灯带亮默认灯效 |
| T-02 | 按键循环 | 短按 KEY2 × 4 次 | solid→flow→breathe→rainbow→solid |
| T-03 | Mesh 配网 | nRF Mesh App → Provision | 配网成功,RTT 打印 Provisioned |
| T-04 | OnOff | App 点 ON/OFF | 灯带亮/灭 |
| T-05 | Lightness | 拖滑块 | 灯带亮度实时跟随 |
| T-06 | Vendor GET | 0xC10059 空参数 | 回复 [id] [count] |
| T-07 | Vendor SET | 0xC20059 + 03 04 | 灯带切 rainbow,RTT 日志 |
| T-08 | 关灯记忆 | 关灯→切亮度→开灯 | 开灯恢复关灯前亮度 |
| T-09 | 并发安全 | 按键同时 App 调亮度 | 无崩溃、状态不乱 |
| T-10 | 无效灯效 | SET effect_id=0xFF | RTT warning,灯效不变 |
6.3 问题排查指南
| 现象 | 最可能原因 | 第一步排查 |
|---|---|---|
| 灯带不亮 | SPI 总线未使能/引脚错 | 逻辑分析仪抓 P0.13 波形 |
| 颜色偏色 | color-mapping 顺序错 | 试 GRB/RGB/BRG |
| App 扫不到 | 蓝牙未启动 | RTT 确认 Bluetooth initialized |
| 配网超时 | 天线/射频 | 靠近手机重试 |
| 配网后无法控制 | 未绑定 AppKey | App → Element 1 → Bind |
| 灯效数=0 | effects.ld 未注入 | 检查 CMakeLists.txt |
| 按键不响应 | GPIOTE 未使能 | DTS 中 &gpiote {status="okay"} |
| 编译缺 led_strip | prj.conf | 检查 CONFIG_LED_STRIP=y |
