1.1 主控板
Arduino Nano RP2040 ConnectMCU:Raspberry Pi RP2040(双核 ARM Cortex-M0+,133 MHz)
封装:Arduino Nano 经典尺寸(18 × 45 mm),兼容 Grove 扩展板
工作电压:3.3 V(逻辑电平 3.3 V,与 Grove 模块同电平域)
模拟输入:8 个 12-bit ADC(本次用 A2 / A3)
数字 IO:20 个,全部支持 INPUT_PULLUP
开发框架:ArduinoCore-mbed(PlatformIO platform = raspberrypi)
1.2 Grove 模块清单
摇杆由两个 10 kΩ 电位器 + 1 个按键组成;
静止 / 居中:X、Y 输出约 500(实际范围 200~800);
推动方向:X、Y 输出 0~1023;
按下按键时,X 被硬件强制置为 1023——所以仅凭 X 轴就能识别"按下",不需要额外的按键信号线。
内部驱动芯片:P9813(全彩 LED 驱动 + 256 级灰度);
2 线协议:CI(时钟)+ DI(数据),开漏输出,模块本身不带板上拉——必须靠 MCU 端上拉(这里使用 RP2040 的内部上拉 + INPUT_PULLUP → OUTPUT 技巧);
单线协议 32 bit / 灯:起始 32 bit 0 + 每灯 32 bit [flag][B][G][R] + 末尾 32 bit 0(末尾 0 即 latch);
支持级联:ChainableLED leds(clkPin, dataPin, numLeds) 中 numLeds 即链上灯数。
1.3 接线总览
Arduino Nano RP2040 Connect ┌────────────────────────────────┐ │ A2 ──┼──► Joystick X(SIG) │ A3 ──┼──► Joystick Y(SIG) │ D6 ──┼──► RGB LED CI (时钟) │ D7 ──┼──► RGB LED DI (数据) └────────────────────────────────┘

二、软件设计(Software Design)
程序结构总览
PlatformIO Project (seeedtest) ├── platformio.ini ├── src/ │ └── main.cpp ← 主程序 └── lib/ └── ChainableLED/ ← 项目级 P9813 驱动库 ├── ChainableLED.h └── ChainableLED.cpp main.cpp 整体扁平结构: main.cpp ├── 文件头注释(接线、行为、Joystick 极性说明) ├── 引脚 / 参数常量 ├── 状态机 + 方向枚举 + "待更新" 标志位 ├── 库对象(ChainableLED leds(6, 7, 1)) ├── 摇杆读取工具(keyPressed / dirFromAxis) ├── onDirChanged() ← 边沿触发加/减档 ├── showColor() / flushColor() ← 待更新标志位模型 ├── setup() ← 引脚 + LED init + 串口 └── loop() ├── 1) 按键清零(写一次) ├── 2) 摇杆方向识别(NONE → INC/DEC 才 +1/-1 档) ├── 3) flushColor() ← 出口检查 pendingUpdate,写一次后清零 └── 4) 节流日志
行为模型
推过死区那一刻立刻 +1 档(边沿触发,不延迟);
保持不动不重复触发(必须物理回中再推一次才能再加);
蓝色通道固定 0(题目要求"双色")。
关键算法:边沿触发 + 待更新标志位
边沿触发的状态机static void onDirChanged(uint8_t &v, Dir cur, Dir &last) {
// 1) 摇杆在死区中心:清零状态机
if (cur == DIR_NONE) {
if (last != DIR_NONE) last = DIR_NONE;
return;
}
// 2) cur == last(保持同一方向):不触发
if (cur == last) return;
// 3) cur != last 且 cur != NONE:NONE -> 非 NONE 上升沿 → 改档 + 置位
last = cur;
if (cur == DIR_INC) v = (v >= LEVELS - 1) ? LEVELS - 1 : v + 1;
else v = (v == 0) ? 0 : v - 1;
pendingUpdate = true;
}死区:raw ∈ [420, 580] 视为 DIR_NONE(中心);
方向:raw < 420 → DIR_DEC;raw > 580 → DIR_INC;
抖动防护:摇杆 ADC 在死区边缘跳变时(580/582/580…),只要没真正回到 NONE,last 一直被锁在 INC/DEC,绝不会重复触发。
static void showColor() {
pendingUpdate = true; // 只置标志,不直接写总线
}
static void flushColor() {
if (!pendingUpdate) return; // 没新数据:什么都不做(LED 锁在原状态)
pendingUpdate = false; // 先清零再写
leds.setColorRGB(0, r, g, 0); // 真正写一次
}数据流:摇杆方向变化 → onDirChanged 改 level → pendingUpdate = true 按键清零 → level = 0 → showColor() → pendingUpdate = true loop() 出口 → flushColor() → 检查标志、写一次、清零优势:
每帧最多写一次 LED;
状态没变完全不写总线(P9813 latch 不会被打断);
判断"何时写"集中在 loop() 出口一处。
库实现关键点(ChainableLED.cpp)
void ChainableLED::setColorRGB(uint8_t led, uint8_t r, uint8_t g, uint8_t b) { sendZeros(); // 起始 32 bit 0 for (uint8_t i = 0; i < _numLeds; ++i) { // 每灯 32 bit if (i == led) sendColor(r, g, b); else sendColor(0, 0, 0); } sendZeros(); // 末尾 32 bit 0(latch) digitalWrite(_clkPin, LOW); delayMicroseconds(80); // latch ≥ 24 µs digitalWrite(_clkPin, HIGH); // 进 idle digitalWrite(_dataPin, HIGH); }起始 32×0 让所有 P9813 走出 reset;
末尾 32×0 + CI LOW 80µs = latch(让 P9813 把 shift register 锁存到输出);
init() 先 INPUT_PULLUP → OUTPUT,让引脚默认电平稳定为 HIGH,避免开漏线浮空。
时间参数
三、功能展示

#include "ChainableLED.h"
ChainableLED::ChainableLED(uint8_t clkPin, uint8_t dataPin, uint8_t numLeds) {
_clkPin = clkPin;
_dataPin = dataPin;
_numLeds = numLeds;
}
void ChainableLED::init() {
pinMode(_clkPin, INPUT_PULLUP);
pinMode(_dataPin, INPUT_PULLUP);
delayMicroseconds(20);
pinMode(_clkPin, OUTPUT);
pinMode(_dataPin, OUTPUT);
digitalWrite(_clkPin, LOW);
digitalWrite(_dataPin, LOW);
delayMicroseconds(50);
setColorRGB(0, 0, 0, 0);
}
void ChainableLED::sendByte(uint8_t b) {
for (uint8_t i = 0; i < 8; ++i) {
if (b & 0x80) digitalWrite(_dataPin, HIGH);
else digitalWrite(_dataPin, LOW);
delayMicroseconds(1);
digitalWrite(_clkPin, HIGH);
delayMicroseconds(1);
digitalWrite(_clkPin, LOW);
b <<= 1;
}
delayMicroseconds(1);
}
void ChainableLED::sendZeros() {
sendByte(0x00);
sendByte(0x00);
sendByte(0x00);
sendByte(0x00);
}
void ChainableLED::sendColor(uint8_t r, uint8_t g, uint8_t b) {
uint8_t prefix = 0xC0;
if ((b & 0x80) == 0) prefix |= 0x20;
if ((b & 0x40) == 0) prefix |= 0x10;
if ((g & 0x80) == 0) prefix |= 0x08;
if ((g & 0x40) == 0) prefix |= 0x04;
if ((r & 0x80) == 0) prefix |= 0x02;
if ((r & 0x40) == 0) prefix |= 0x01;
sendByte(prefix);
sendByte(b);
sendByte(g);
sendByte(r);
}
void ChainableLED::setColorRGB(uint8_t led, uint8_t r, uint8_t g, uint8_t b) {
if (led >= _numLeds) return;
sendZeros();
for (uint8_t i = 0; i < _numLeds; ++i) {
if (i == led) sendColor(r, g, b);
else sendColor(0, 0, 0);
}
sendZeros();
digitalWrite(_clkPin, LOW);
delayMicroseconds(80);
digitalWrite(_clkPin, HIGH);
digitalWrite(_dataPin, HIGH);
}
// HSL -> RGB 转换 + 写入(保留以匹配官方 API)
void ChainableLED::setColorHSL(uint8_t led, float hue, float sat, float lum) {
if (hue < 0.0f) hue = 0.0f;
if (hue > 1.0f) hue = 1.0f;
if (sat < 0.0f) sat = 0.0f;
if (sat > 1.0f) sat = 1.0f;
if (lum < 0.0f) lum = 0.0f;
if (lum > 1.0f) lum = 1.0f;
uint8_t r, g, b;
if (sat == 0.0f) {
uint8_t v = (uint8_t)(lum * 255.0f);
r = g = b = v;
} else {
float h = hue * 6.0f;
int i = (int)h;
float f = h - i;
float p = lum * (1.0f - sat);
float q = lum * (1.0f - sat * f);
float t = lum * (1.0f - sat * (1.0f - f));
float rv, gv, bv;
switch (i % 6) {
case 0: rv = lum; gv = t; bv = p; break;
case 1: rv = q; gv = lum; bv = p; break;
case 2: rv = p; gv = lum; bv = t; break;
case 3: rv = p; gv = q; bv = lum; break;
case 4: rv = t; gv = p; bv = lum; break;
default: rv = lum; gv = p; bv = q; break;
}
r = (uint8_t)(rv * 255.0f);
g = (uint8_t)(gv * 255.0f);
b = (uint8_t)(bv * 255.0f);
}
setColorRGB(led, r, g, b);
}
我要赚赏金
