修复律动期间 UART0 TX 拥堵 + 补充踩坑文档 + 硬件文档归类 + Mac 开发工程入库

一、代码修复(HOLOMAIN.ino)
1、processCommand 响应改为只发 Serial (USB CDC / Windows 调试),不再回发 SerialLinux (Android)
   背景:Android 随音乐律动高频发送 LED 命令时,ESP32 每条命令的响应挤占 UART0 TX,
         导致刷卡产生的 SORC_xxx 业务数据延迟到达 Android,表现为律动期间刷卡经常无效
   效果:UART0 TX 释放给业务数据使用,Windows 调试仍能通过 USB CDC 看到响应

二、踩坑经验文档补充(ESP32踩坑经验文档.md)
1、新增坑 12:音乐律动时高频 LED 命令导致 RFID 高概率失败【物理层面干扰】
2、内容涵盖:
   - 现象描述与触发条件(律动期间 vs 静态场景)
   - 根本原因推演:高频亮度跳变 → WS2812 电流瞬变 → 电源噪声耦合 → RC522 SPI 异常
   - 与坑 7(WS2812 关中断)的区别:坑 12 是物理电源噪声,即使用 RMT 也无法避免
   - 四层解决方案按优先级排列:
     方案1 Android 端降频+去重+节拍化(已验证,60FPS→5次/秒,失败率显著降低)
     方案2 ESP32 响应只发调试口(已实施,本次提交)
     方案3 ESP32 亮度平滑过渡(预备方案,含完整代码和 step_max 调校参考)
     方案4 RC522 供电去耦电容(硬件方案,终极根治)
3、经验教训:主机端源头减量 > 固件端平滑过渡 > 硬件去耦电容

三、硬件文档目录归类(docs/)
1、新建 docs/ESP32/ 分类目录
2、新建 docs/OrangePi_CM5/ 分类目录
3、移动原散落在 docs/ 根目录的文档到对应分类
4、新增资料:
   - docs/OrangePi_CM5/OPI CM5 BASE-TABLET_V1_1_SCH.pdf(底板原理图)
   - docs/OrangePi_CM5/开发工具用户手册_v1.0.pdf

四、Mac 开发工程入库(Luotianyi_Mac/)
1、新增 Luotianyi_Mac/Luotianyi_Mac.ino
2、用途:Windows 切换到 Mac 开发环境后,后续 ESP32 业务在此工程继续开发
3、HOLOMAIN.ino 保留作为 Windows Arduino IDE 下的基线版本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-04-22 14:35:42 +08:00
parent 90105a60ea
commit 713cbd1835
9 changed files with 1017 additions and 2 deletions

View File

@ -411,6 +411,107 @@ void MFRC522::PCD_Init() {
---
### 坑 12音乐律动时高频 LED 命令导致 RFID 高概率失败【物理层面干扰】
**现象**
- Android 刷卡后播放音乐,**随音乐律动每帧30-60 FPS发送 LED 亮度控制命令**给 ESP32
- 律动期间再次刷 NFC 卡切换形象 → **高频刷卡无效**
- 音乐停止或非律动场景 → 刷卡正常
**根本原因(电源噪声耦合干扰 SPI**
1. **命令高频引起亮度剧烈跳变**
- Android 每 17-33ms 发一条 `MO_BRI_xx`,值在 30~80 之间跳变
- 每条命令到达 ESP32 后立即修改 `led2Brightness`
- LED 任务每 50ms 执行一次 `FastLED.show()`,每次用**当时的亮度值**点亮 186 颗 WS2812
2. **WS2812 刷新瞬时电流剧烈变化**
- 186 颗 × 24mA50% 白光)= 4.5A 级别
- 每 50ms 亮度从 30% 跳到 80%:瞬时电流 ±2-3A
- 电流变化率 dI/dt 极大
3. **电源轨产生高频噪声耦合到 RC522 SPI**
- 3.3V 轨上 ΔV = L × dI/dt 产生 200mV+ 的瞬时跌落
- RC522 SPI 时钟/数据信号幅度 3.3V,电源噪声直接破坏 SPI 时序
- 症状:`Error in communication` / `CRC_A does not match` / `Collision detected`
**与"坑 7WS2812 关中断干扰 SPI" 的区别**
- 坑 7CPU 层面的时序干扰(关中断期间 SPI 任务无法调度)
- 坑 12**物理层面的电源噪声干扰**(即使用 RMT 不关中断也无法避免)
- 高频动态亮度变化是"坑 12"的特有触发条件,静态场景下不出现
**三层解决方案(按优先级)**
**方案 1Android 端降频 + 去重(根治,已验证)**
- 从每帧发送30-60 FPS降低到 **5-10 次/秒**
- 去重:当前亮度值与上次相同不发送
- 节拍化:只在音乐节拍转折点发送命令(而非每帧)
- 变化阈值:亮度变化 < 5% 不发送
- 独立接收线程Android 端 UART RX 和 TX 用不同线程,避免 RX 被阻塞
- **实测**:从 60 FPS 降到 5 次/秒后,失败率从"高频失败"降到"偶发失败"
**方案 2ESP32 端 processCommand 响应不回发 Android已实施**
```cpp
// 命令响应统一发到 Serial (USB CDC / Windows 调试),不回发 SerialLinux (Android)
void processCommand(const String& command, Stream& /*src*/) {
Stream& resp = Serial; // 只发 Windows 调试口
...
}
```
- 原因Android 每条命令都回响应会堵塞 UART0 TX导致 `SORC_xxx` 业务数据延迟到达
- 效果UART0 TX 空闲,业务数据立即送达 Android
**方案 3预备ESP32 端 LED 亮度平滑过渡(当前未启用)**
**核心思路**:把"瞬间跳变"改成"平滑过渡",降低电流变化率 dI/dt。
**实现要点**
```cpp
// 全局新增目标亮度变量
uint8_t led2TargetBrightness = 102;
// 命令处理改为设置目标值(不直接改 led2Brightness
// L468 附近 MO_BRI_ 处理:
led2TargetBrightness = brightnessMap[level];
// LED 任务循环内加渐进逻辑(在 FastLED.show() 之前):
const uint8_t BRIGHTNESS_STEP_MAX = 25; // 单次最大变化量
if (led2Brightness != led2TargetBrightness) {
int delta = (int)led2TargetBrightness - (int)led2Brightness;
if (abs(delta) <= BRIGHTNESS_STEP_MAX) {
led2Brightness = led2TargetBrightness;
} else {
led2Brightness += (delta > 0 ? BRIGHTNESS_STEP_MAX : -BRIGHTNESS_STEP_MAX);
}
}
```
**参数调校参考**
- `step_max = 25`150 的亮度差需要 6 步300ms完成平滑但偶有迟滞
- `step_max = 50`3 步150ms完成跟随度好但平滑程度降低
- 选择原则:每步变化 / 总变化 ≤ 1/3保证电流变化率降低 3 倍以上
**预期效果**
- 电流变化率降低 5-8 倍 → 电源噪声显著减小 → RFID 失败率进一步降低
- 代价:亮度响应延迟 100-300ms极端情况下"砰"的瞬间闪光效果会变成"啪"的渐亮
**什么时候启用**
- 当前 Android 降频到 5 次/秒已能接受的失败率,方案 3 暂不启用
- 触发条件:若 Android 进一步降频仍不够,或业务要求失败率 < 1%启用方案 3
**方案 4终极需硬件改动**RC522 供电去耦
- 3.3V 端加 100μF 低 ESR 电解 + 10μF 钽 + 100nF 瓷片组合电容
- WS2812 电源和 RC522 电源分开走线
- 或给 RC522 加独立 LDO 稳压
**经验教训**
> **高频动态电流负载 + 敏感数字外设共用电源 = 必然的干扰**。软件层只能缓解,不能根治。产品级方案必须硬件去耦或电源隔离。
>
> **优化优先级**:主机端源头减量 > 固件端平滑过渡 > 硬件去耦电容。源头减少干扰比在受害端补救更有效。
---
## 三、最终架构
```

View File

@ -400,8 +400,12 @@ void TaskBTN2code(void* pvParameters) {
}
// 处理单条完整命令
// 参数 resp 指向发送方串口命令响应会回到发送方Windows 发命令 Windows 收响应Android 同理)
void processCommand(const String& command, Stream& resp) {
// 响应统一发到 Serial (USB CDC / Windows 调试口),不回发给 SerialLinux (Android)
// 原因Android 高频发灯光命令(音乐律动)时,若每条命令都回响应到 UART0 TX
// 会堵塞业务数据 SORC_xxx / SO_BTx 的发送,导致刷卡后 Android 收到延迟甚至丢失
// 参数 src 仅用于保留调用兼容性,响应不再写入 src
void processCommand(const String& command, Stream& /*src*/) {
Stream& resp = Serial; // 所有命令响应只发 Windows 调试口
if (command.startsWith("MO_LED_")) {
String modeStr = command.substring(7);
int newMode = modeStr.toInt();

View File

@ -0,0 +1,910 @@
#include <SPI.h>
#include <MFRC522.h>
#include <FastLED.h>
#include <Arduino.h>
#include <driver/ledc.h>
// RFID引脚定义
#define RFID_RST_PIN 14 // RC522 复位引脚
#define RFID_SS_PIN 10 // RC522 CS/SDA引脚SPI和I2C共用
#define RFID_MISO_PIN 13 // MISO 引脚
#define RFID_MOSI_PIN 12 // MOSI 引脚
#define RFID_SCK_PIN 11 // SCK 引脚
// LED定义
#define LED_PIN_1 4 // 1颗WS2812灯珠引脚
#define LED_PIN_2 5 // 160颗WS2812灯带引脚控制灯珠颜色
#define LED_PIN_3 48 // 1颗WS2812灯珠引脚新增
#define LED_COUNT_1 1 // 1颗灯珠
#define LED_COUNT_2 186 // 160颗灯带
#define LED_COUNT_3 1 // 1颗灯珠新增
// PWM定义
#define PWM_PIN 6 // PWM输出控制输出PWM功率
#define PWM_CHANNEL 0 // PWM通道
#define PWM_FREQ 1000 // PWM频率(Hz)
#define PWM_RESOLUTION 10 // PWM分辨率(位)
#define DEFAULT_DUTY 819 // 默认占空比(80%)
// 按钮和输入引脚定义
#define BTN0_PIN 15 // 按钮0引脚
#define WAKEUP1_PIN 16 // 唤醒引脚1
#define BTN1_PIN 17 // 按钮1引脚
#define BTN2_PIN 18 // 按钮2引脚
// 任务句柄
TaskHandle_t TaskRFID, TaskLED1, TaskLED2, TaskLED3, TaskPWM, TaskBTN0, TaskWAKEUP1, TaskBTN1, TaskBTN2;
// 双串口架构:
// - Serial = USB-Serial-JTAG (USB2 口),连 Windows 做调试日志
// - SerialLinux = UART0 (CH343/USB1 口),连 Android 开发板收发业务数据
// UART0 默认引脚TX=GPIO43、RX=GPIO44对应 CH343P 的 RXD/TXD
//
// ⚠️ 重要:必须用 #define 别名引用 Arduino core 自带的 Serial0 对象
// 不能自己创建 HardwareSerial(0),否则 Arduino core 的 UART0 RX 中断会把
// 数据放进 Serial0 的 ring buffer而自建对象的 available() 读不到 → 命令无响应
#define SerialLinux Serial0
// Serial 输出互斥锁:防止多任务并发写串口导致数据交错/截断
SemaphoreHandle_t serialMutex = NULL;
// 业务数据输出:只发到 UART0 (给 Android 开发板)
// 调用方RFID 任务发 SORC_xxx按键任务发 SO_xxx
// 为什么不发 Serial (USB CDC)
// 1. 生产场景只连 AndroidWindows 不连Serial 写入+flush 在无 host 时可能阻塞
// 2. 阻塞会拖慢 RFID 响应(之前刷卡不灵敏的潜在原因之一)
// 3. UART0 是广播式通讯,无 host 也能正常写入 FIFO不阻塞
// 互斥锁保留:虽然只发一个串口,但防止多任务并发调用时缓冲混乱
void serialPrintlnSafe(const String& msg) {
if (serialMutex && xSemaphoreTake(serialMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
SerialLinux.println(msg);
SerialLinux.flush(); // UART0 硬件 FIFO 128 字节115200 波特率约 1ms 完成
xSemaphoreGive(serialMutex);
} else {
// 降级路径:拿不到锁(极少发生)
SerialLinux.println(msg);
}
}
// 全局变量
MFRC522 rfid(RFID_SS_PIN, RFID_RST_PIN); // 创建RFID实例
CRGB leds1[LED_COUNT_1]; // 1颗灯珠数组
CRGB leds2[LED_COUNT_2]; // 160颗灯带数组
CRGB leds3[LED_COUNT_3]; // 1颗灯珠数组新增
CRGB frozenLeds2[LED_COUNT_2]; // 保存冻结时的颜色数据模式5专用
uint8_t frozenBrightness = 255; // 保存冻结时的亮度值,用于计算相对亮度比例
String lastCardData = ""; // 上次读取的RFID卡数据
int ledMode = 1; // 灯带模式默认为1(白色)
int pwmDuty = DEFAULT_DUTY; // PWM占空比
bool btn0State = HIGH; // 按钮0状态
bool btn0LongPress = false; // 按钮0长按标志
bool wakeup1State = LOW; // 唤醒引脚1状态
bool btn1State = LOW; // 按钮1状态
bool btn2State = LOW; // 按钮2状态
int singleLedMode = 7; // 单颗LED模式默认为7(白色)
// 灯带动画全局变量
static uint8_t rainbowHue = 0;
static int trainPos = 0;
static unsigned long lastUpdate = 0;
static const int TRAIN_LENGTH = 16; // 火车灯长度
static int trainPhase = 0; // 火车阶段0-正向出站1-正向前进2-正向进站3-反向出站4-反向前进5-反向进站
static const int VIRTUAL_LED_COUNT = LED_COUNT_2 + TRAIN_LENGTH; // 虚拟灯带长度
// LED亮度线性映射表 (0~100 → 26~255) - 最小阈值10%
// 用于将用户输入的0-100%亮度值映射到实际的PWM值
// 避免过低亮度导致LED完全不可见的问题
const uint8_t brightnessMapLinear[101] = {
0, 28, 31, 33, 36, 38, 41, 43, 46, 48, // 0-9
51, 54, 56, 59, 61, 64, 66, 69, 71, 74, // 10-19
77, 79, 82, 84, 87, 89, 92, 94, 97, 99, // 20-29
102, 105, 107, 110, 112, 115, 117, 120, 122, 125, // 30-39
128, 130, 133, 135, 138, 140, 143, 145, 148, 150, // 40-49
153, 156, 158, 161, 163, 166, 168, 171, 173, 176, // 50-59
179, 181, 184, 186, 189, 191, 194, 196, 199, 201, // 60-69
204, 207, 209, 212, 214, 217, 219, 222, 224, 227, // 70-79
230, 232, 235, 237, 240, 242, 245, 247, 250, 252, // 80-89
253, 254, 254, 254, 255, 255, 255, 255, 255, 255, // 90-99
255 // 100%
};
// 全局选择映射表
const uint8_t* brightnessMap = brightnessMapLinear;
// LED2亮度控制0-255
// 用于控制LED灯带的整体亮度影响模式1、2、4和5
// 模式3使用独立的呼吸算法基于此值计算动态亮度范围
uint8_t led2Brightness = 102; // 默认40%左右102/255≈40%
// 单颗LED颜色数组
CRGB singleLedColors[8] = {
CRGB::Black, // 0: 熄灭
CRGB::Blue, // 1: 蓝色
CRGB::Green, // 2: 绿色
CRGB::Orange, // 3: 橙色
CRGB::Red, // 4: 红色
CRGB::Purple, // 5: 紫色
CRGB::Yellow, // 6: 黄色
CRGB::White // 7: 白色
};
// RC522 硬件复位(运行时调用,不依赖 MFRC522 库的 RST 自动判断)
// 用于 setup 初始化和运行时检测到 RC522 异常时恢复
void rc522HardResetRuntime() {
pinMode(RFID_RST_PIN, OUTPUT);
digitalWrite(RFID_RST_PIN, LOW);
delay(10);
digitalWrite(RFID_RST_PIN, HIGH);
delay(50);
}
// RFID读取任务
void TaskRFIDcode(void* pvParameters) {
// 运行时健康检查:每 5 秒读一次 VersionReg检测到异常自动恢复
// 防止运行过程中 RC522 因电源波动、WS2812 大电流干扰等原因进入异常状态
static uint32_t lastHealthCheck = 0;
const uint32_t HEALTH_CHECK_INTERVAL_MS = 5000;
for (;;) {
// 运行时健康检查(和下面的轮询并行)
uint32_t now = millis();
if (now - lastHealthCheck > HEALTH_CHECK_INTERVAL_MS) {
lastHealthCheck = now;
byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
if (version != 0x91 && version != 0x92) {
// 通讯异常,自动恢复
Serial.print("RC522 health check failed (VersionReg=0x");
Serial.print(version, HEX);
Serial.println("), reinitializing...");
rc522HardResetRuntime();
rfid.PCD_Init();
}
}
// 寻找新卡片
if (!rfid.PICC_IsNewCardPresent()) {
delay(10);
continue;
}
// 验证NUID是否可读
if (!rfid.PICC_ReadCardSerial()) {
delay(10);
continue;
}
// 读取卡片数据(用户数据区)
String cardData = "";
MFRC522::MIFARE_Key key;
// 准备认证密钥
for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
// 选择卡片
MFRC522::StatusCode status;
status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &(rfid.uid));
if (status != MFRC522::STATUS_OK) {
// 调试日志用普通 Serial.println失败频率较高时避免 flush 阻塞拖慢 RFID 响应
// 偶尔截断可接受Linux 端用正则 ^SORC_HA\d+$ 过滤业务数据即可)
Serial.println(String("Authentication failed: ") + rfid.GetStatusCodeName(status));
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
delay(30); // 从 100ms 降到 30ms提升刷卡响应速度
continue;
}
// 读取数据块
byte buffer[18];
byte size = sizeof(buffer);
status = rfid.MIFARE_Read(4, buffer, &size);
if (status != MFRC522::STATUS_OK) {
Serial.println(String("Reading failed: ") + rfid.GetStatusCodeName(status));
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
delay(30); // 从 100ms 降到 30ms
continue;
}
// 转换为ASCII字符串
for (byte i = 0; i < 16; i++) {
if (buffer[i] >= 32 && buffer[i] <= 126) { // 可打印ASCII字符
cardData += (char)buffer[i];
}
}
// 移除空白字符
cardData.trim();
// 卡片数据格式校验:规则 "HA" + 阿拉伯数字
// 过滤掉卡片读取异常或数据损坏的情况,避免发送无效数据给 Linux
auto isValidCardData = [](const String& d) -> bool {
if (d.length() < 3) return false; // 至少 "HA" + 1 位数字
if (!d.startsWith("HA")) return false; // 必须以 HA 开头
for (size_t i = 2; i < d.length(); i++) {
if (!isdigit(d[i])) return false; // HA 后面必须全是数字
}
return true;
};
// 3 秒去重窗口:同一张卡 3 秒内只发送一次,超过后允许重发
// 切换到不同卡立即发送
static String lastSentCard = "";
static unsigned long lastSentTime = 0;
const unsigned long DUPLICATE_WINDOW_MS = 3000;
if (!cardData.isEmpty() && isValidCardData(cardData)) {
unsigned long now = millis();
bool isDuplicate = (cardData == lastSentCard) && (now - lastSentTime < DUPLICATE_WINDOW_MS);
if (!isDuplicate) {
serialPrintlnSafe("SORC_" + cardData);
lastSentCard = cardData;
lastSentTime = now;
}
}
// 使放置在读卡区的IC卡进入休眠状态不再重复读卡
rfid.PICC_HaltA();
// 停止加密PCD
rfid.PCD_StopCrypto1();
delay(100);
}
}
// 注意以下两个函数已被TaskLEDUnifiedCode替代保留仅供参考
// 实际运行中不会被调用因为setup()中没有创建对应的任务
// LED1控制任务已废弃由TaskLEDUnifiedCode统一处理
void TaskLED1code(void* pvParameters) {
// 此函数已被废弃,不再使用
// LED1的控制已集成到TaskLEDUnifiedCode中
vTaskDelete(NULL); // 如果意外创建了此任务,立即删除
}
// LED3控制任务已废弃由TaskLEDUnifiedCode统一处理
void TaskLED3code(void* pvParameters) {
// 此函数已被废弃,不再使用
// LED3的控制已集成到TaskLEDUnifiedCode中
vTaskDelete(NULL); // 如果意外创建了此任务,立即删除
}
// PWM控制任务
void TaskPWMcode(void* pvParameters) {
for (;;) {
// 设置PWM占空比
ledc_set_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)PWM_CHANNEL, pwmDuty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)PWM_CHANNEL);
delay(100);
}
}
// 按钮0检测任务
void TaskBTN0code(void* pvParameters) {
static unsigned long pressStartTime = 0;
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN0_PIN);
btn0State = lastState;
for (;;) {
bool currentState = digitalRead(BTN0_PIN);
// 检测下降沿(按下)
if (lastState == HIGH && currentState == LOW) {
pressStartTime = millis();
btn0State = LOW;
serialPrintlnSafe("SO_BT0_HIGH");
btn0LongPress = false;
}
// 检测上升沿(释放)
else if (lastState == LOW && currentState == HIGH) {
btn0State = HIGH;
serialPrintlnSafe("SO_BT0_LOW");
btn0LongPress = false;
}
// 检测长按
else if (currentState == LOW && millis() - pressStartTime >= 2000 && !btn0LongPress) {
btn0LongPress = true;
serialPrintlnSafe("SO_BT0_HIGHL");
}
lastState = currentState;
delay(10);
}
}
// WAKEUP1检测任务
void TaskWAKEUP1code(void* pvParameters) {
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(WAKEUP1_PIN);
wakeup1State = lastState;
for (;;) {
bool currentState = digitalRead(WAKEUP1_PIN);
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
wakeup1State = HIGH;
serialPrintlnSafe("SO_WAKEUP1");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
wakeup1State = LOW;
serialPrintlnSafe("SO_WAKEUP0");
}
lastState = currentState;
delay(10);
}
}
// 按钮1检测任务
void TaskBTN1code(void* pvParameters) {
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN1_PIN);
btn1State = lastState;
for (;;) {
bool currentState = digitalRead(BTN1_PIN);
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
btn1State = HIGH;
serialPrintlnSafe("SO_BT1_HIGH");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
btn1State = LOW;
serialPrintlnSafe("SO_BT1_LOW");
}
lastState = currentState;
delay(10);
}
}
// 按钮2检测任务
void TaskBTN2code(void* pvParameters) {
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN2_PIN);
btn2State = lastState;
for (;;) {
bool currentState = digitalRead(BTN2_PIN);
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
btn2State = HIGH;
serialPrintlnSafe("SO_BT2_HIGH");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
btn2State = LOW;
serialPrintlnSafe("SO_BT2_LOW");
}
lastState = currentState;
delay(10);
}
}
// 处理单条完整命令
// 响应统一发到 Serial (USB CDC / Windows 调试口),不回发给 SerialLinux (Android)
// 原因Android 高频发灯光命令(音乐律动)时,若每条命令都回响应到 UART0 TX
// 会堵塞业务数据 SORC_xxx / SO_BTx 的发送,导致刷卡后 Android 收到延迟甚至丢失
// 参数 src 仅用于保留调用兼容性,响应不再写入 src
void processCommand(const String& command, Stream& /*src*/) {
Stream& resp = Serial; // 所有命令响应只发 Windows 调试口
if (command.startsWith("MO_LED_")) {
String modeStr = command.substring(7);
int newMode = modeStr.toInt();
if (newMode >= 0 && newMode <= 7) {
singleLedMode = newMode;
resp.print("Single LED set to mode: ");
resp.println(newMode);
} else {
resp.println("Invalid single LED mode command");
}
} else if (command.startsWith("MO_LEDN_")) {
String modeStr = command.substring(8);
int newMode = modeStr.toInt();
if (newMode >= 0 && newMode <= 5) {
if (led2Brightness == 0) {
resp.println("当前亮度为0请先将亮度调整至0以上再切换显示模式");
} else {
ledMode = newMode;
if (newMode == 4) {
trainPos = -TRAIN_LENGTH;
trainPhase = 0;
rainbowHue = random8();
}
if (newMode == 5) {
memcpy(frozenLeds2, leds2, sizeof(leds2));
frozenBrightness = led2Brightness;
}
resp.print("LED strip set to mode: ");
resp.println(newMode);
}
} else {
resp.println("Invalid LED strip mode command");
}
} else if (command.startsWith("MO_PWM_")) {
String dutyStr = command.substring(7);
int newDuty = dutyStr.toInt();
if (newDuty == 1) {
pwmDuty = 1023;
} else if (newDuty == 0 || newDuty == 20 || newDuty == 40 || newDuty == 60 || newDuty == 80) {
pwmDuty = (newDuty * 1023) / 100;
} else {
resp.println("Invalid PWM command");
}
resp.print("PWM set to: ");
resp.print((pwmDuty * 100) / 1023);
resp.println("%");
} else if (command.startsWith("MO_BRI_")) {
String levelStr = command.substring(7);
levelStr.trim();
if (levelStr.length() == 0) {
resp.println("错误: 缺少亮度值");
return;
}
bool isNumeric = true;
for (char c : levelStr) {
if (!isdigit(c)) { isNumeric = false; break; }
}
if (!isNumeric) {
resp.println("错误: 亮度值必须为整数");
return;
}
int level = levelStr.toInt();
if (level >= 0 && level <= 100) {
led2Brightness = brightnessMap[level];
resp.print("LED亮度: ");
resp.print(level);
resp.println("%");
} else {
resp.println("错误: 亮度值需在0-100之间");
}
} else if (command == "RESET") {
// 软复位响应同时发到两个串口Windows 和 Android 都能看到重启日志)
Serial.println("System resetting...");
SerialLinux.println("System resetting...");
Serial.flush();
SerialLinux.flush();
delay(100);
ESP.restart();
}
}
// 从指定流读取命令字节,累积到完整一行后交给 processCommand 处理
// 每个流需要独立的命令缓冲区(通过引用传入 static 变量保持状态)
void handleCommandFromStream(Stream& src, String& cmdBuf) {
while (src.available()) {
// 命令长度保护:防止恶意或异常数据撑爆内存
if (cmdBuf.length() > 64) {
src.println("错误: 命令过长最大64字符");
cmdBuf = "";
while (src.available()) src.read();
return;
}
char c = src.read();
if (c == '\n') {
cmdBuf.trim(); // 去掉末尾 \r 兼容不同行尾符
if (cmdBuf.length() > 0) {
processCommand(cmdBuf, src);
}
cmdBuf = "";
} else {
cmdBuf += c;
}
}
}
// 统一LED控制任务防闪烁优化版本
// 集中管理LED1单颗、LED2灯带、LED3强制关闭的显示逻辑
// 优化特性:
// 1. 30FPS稳定更新频率防止闪烁
// 2. 修复模式5的双重亮度衰减问题
// 3. 统一亮度处理机制
// 4. 内存优化,减少不必要的数据拷贝
// 5. 防闪烁机制确保LED显示稳定
void TaskLEDUnifiedCode(void* pvParameters) {
static unsigned long lastLEDUpdate = 0;
// 20FPS 更新频率:兼顾视觉流畅度和 RFID 稳定性
// 每秒 20 次 WS2812 传输(每次 ~5.6ms 关中断),相比 30FPS 减少 33% 干扰窗口
const unsigned long LED_UPDATE_INTERVAL = 50;
for (;;) {
unsigned long currentTime = millis();
// 控制更新频率避免过度占用CPU和闪烁问题
if (currentTime - lastLEDUpdate < LED_UPDATE_INTERVAL) {
delay(5); // 增加延时,确保任务调度稳定
continue;
}
lastLEDUpdate = currentTime;
// ---- LED1 控制(单颗 LED----
if (singleLedMode >= 0 && singleLedMode <= 7) {
leds1[0] = singleLedColors[singleLedMode];
} else {
leds1[0] = CRGB::Blue;
}
// ---- LED3 控制(熄灭)----
leds3[0] = CRGB::Black;
// ---- LED2 控制(灯带)----
switch (ledMode) {
case 0: // 模式0全部熄灭关闭所有LED灯珠
fill_solid(leds2, LED_COUNT_2, CRGB::Black);
break;
case 1: // 模式1纯白色静态光亮度可通过led2Brightness调节
fill_solid(leds2, LED_COUNT_2, CHSV(0, 0, led2Brightness));
break;
case 2: // 模式2彩虹流水灯颜色沿灯带流动速度和亮度可调
for (int i = 0; i < LED_COUNT_2; i++) {
leds2[i] = CHSV(rainbowHue + i * 256 / LED_COUNT_2, 255, led2Brightness);
}
rainbowHue++;
break;
case 3: // 模式3彩虹呼吸灯优化版本缓慢变色配合呼吸效果
{
static unsigned long lastHueUpdate = 0;
static unsigned long lastBreathUpdate = 0;
static uint8_t breathingHue = 0;
static uint8_t breathPhase = 0;
unsigned long currentTime = millis();
// 每300ms更新一次色相实现非常缓慢的颜色变化
if (currentTime - lastHueUpdate > 300) {
breathingHue += 1;
lastHueUpdate = currentTime;
}
// 每30ms更新一次呼吸相位控制亮度变化节奏
if (currentTime - lastBreathUpdate > 30) {
breathPhase += 2;
lastBreathUpdate = currentTime;
}
// 计算呼吸亮度基于led2Brightness的60%-100%范围,避免过暗
uint8_t minBrightness = led2Brightness * 60 / 100;
uint8_t maxBrightness = led2Brightness;
uint8_t breathValue = map(sin8(breathPhase), 0, 255, minBrightness, maxBrightness);
for(int i = 0; i < LED_COUNT_2; i++) {
leds2[i] = CHSV(breathingHue, 200, breathValue);
}
}
break;
case 4: // 模式4彩虹火车灯模拟火车往返运行的动态效果
if (millis() - lastUpdate > 30) { // 30ms更新间隔控制火车移动速度
lastUpdate = millis();
fill_solid(leds2, LED_COUNT_2, CRGB::Black);
switch (trainPhase) {
case 0: // 阶段0正向出站火车从起点逐渐显现
for (int i = 0; i < TRAIN_LENGTH; i++) {
int pos = trainPos + i;
if (pos >= 0 && pos < LED_COUNT_2) {
uint8_t hue = rainbowHue + (i * 256 / TRAIN_LENGTH);
leds2[pos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= 0) {
trainPhase = 1; // 切换到正向前进阶段
trainPos = 0;
}
break;
case 1: // 阶段1正向前进火车完整显示并向终点移动
for (int i = 0; i < TRAIN_LENGTH; i++) {
int pos = trainPos + i;
if (pos >= 0 && pos < LED_COUNT_2) {
uint8_t hue = rainbowHue + (i * 256 / TRAIN_LENGTH);
leds2[pos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= LED_COUNT_2 - TRAIN_LENGTH) {
trainPhase = 2; // 切换到正向进站阶段
trainPos = LED_COUNT_2 - TRAIN_LENGTH;
}
break;
case 2: // 阶段2正向进站火车从尾部开始消失
for (int i = 0; i < TRAIN_LENGTH; i++) {
int displayPos = LED_COUNT_2 - 1 - i;
if (displayPos >= trainPos) {
uint8_t hue = rainbowHue + (i * 256 / TRAIN_LENGTH);
leds2[displayPos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= LED_COUNT_2) {
trainPhase = 3; // 切换到反向出站阶段
trainPos = 0;
rainbowHue += 64; // 改变彩虹颜色,增加视觉变化
}
break;
case 3: // 阶段3反向出站火车从终点逐渐显现
for (int i = 0; i < trainPos + 1; i++) {
int pos = LED_COUNT_2 - 1 - i;
if (pos >= 0) {
uint8_t hue = rainbowHue + ((TRAIN_LENGTH - 1 - i) * 256 / TRAIN_LENGTH);
leds2[pos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= TRAIN_LENGTH) {
trainPhase = 4; // 切换到反向前进阶段
trainPos = TRAIN_LENGTH;
}
break;
case 4: // 阶段4反向前进火车完整显示并向起点移动
for (int i = 0; i < TRAIN_LENGTH; i++) {
int pos = LED_COUNT_2 - trainPos + i;
if (pos >= 0 && pos < LED_COUNT_2) {
uint8_t hue = rainbowHue + ((TRAIN_LENGTH - 1 - i) * 256 / TRAIN_LENGTH);
leds2[pos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= LED_COUNT_2) {
trainPhase = 5; // 切换到反向进站阶段
trainPos = 0;
}
break;
case 5: // 阶段5反向进站火车从头部开始消失
for (int i = 0; i < TRAIN_LENGTH - trainPos; i++) {
int pos = i;
if (pos < LED_COUNT_2) {
uint8_t hue = rainbowHue + ((TRAIN_LENGTH - 1 - i) * 256 / TRAIN_LENGTH);
leds2[pos] = CHSV(hue, 255, led2Brightness);
}
}
trainPos++;
if (trainPos >= TRAIN_LENGTH) {
trainPhase = 0; // 重新开始正向出站,形成循环
trainPos = -TRAIN_LENGTH;
rainbowHue += 64; // 再次改变彩虹颜色
}
break;
}
}
break;
case 5: // 模式5冻结当前灯效保持切换时的图像但允许调节亮度内存优化版本
if (led2Brightness == 0) {
fill_solid(leds2, LED_COUNT_2, CRGB::Black); // 亮度为0时完全熄灭
} else {
// 计算相对亮度比例,避免双重衰减问题
uint16_t brightnessRatio = (uint16_t)led2Brightness * 255 / frozenBrightness;
if (brightnessRatio > 255) brightnessRatio = 255;
// 直接计算并设置像素颜色内存优化避免使用memcpy
for (int i = 0; i < LED_COUNT_2; i++) {
leds2[i].r = (frozenLeds2[i].r * brightnessRatio) >> 8;
leds2[i].g = (frozenLeds2[i].g * brightnessRatio) >> 8;
leds2[i].b = (frozenLeds2[i].b * brightnessRatio) >> 8;
}
}
break;
}
// ---- 最终统一刷新LED ----
// 添加FastLED刷新保护确保数据稳定后再显示
FastLED.show();
// ---- 稳定的延时机制 ----
// 使用固定延时确保LED显示稳定避免闪烁
delay(10); // 10ms延时确保LED数据传输完成
}
}
void setup() {
// 初始化 USB CDC 串口Windows 调试)
// 增大 TX 缓冲区到 4KB防止多任务并发写串口时 USB CDC 默认缓冲区溢出导致数据截断
Serial.setTxBufferSize(4096);
Serial.begin(115200);
// 初始化 UART0CH343/USB1 → Linux 业务通讯)
// 默认引脚TX=GPIO43、RX=GPIO44波特率与 Linux 端保持一致
SerialLinux.begin(115200);
// 创建 Serial 输出互斥锁
serialMutex = xSemaphoreCreateMutex();
Serial.println("System starting...");
SerialLinux.println("System starting...");
// 初始化SPI总线
SPI.begin(RFID_SCK_PIN, RFID_MISO_PIN, RFID_MOSI_PIN, RFID_SS_PIN);
// 初始化RFID显式硬件复位 + 版本校验 + 失败重试
// 背景MFRC522 库的 PCD_Init 自动判断 RST 电平,冷启动时 GPIO14 浮空可能读到 HIGH
// 库只做软件复位但 RC522 尚未完成上电 → 芯片卡在异常状态 → 刷卡永远失败。
// 显式拉低再拉高 RST 可避免这个坑。GitHub miguelbalboa/rfid #229 #269 记录此问题。
// 复用顶部定义的 rc522HardResetRuntime() 函数
// 最多重试 3 次初始化,直到版本寄存器返回合法值
bool rfidReady = false;
for (uint8_t attempt = 1; attempt <= 3; attempt++) {
rc522HardResetRuntime(); // 显式拉低再拉高 RST
rfid.PCD_Init();
byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
Serial.print("RC522 init attempt ");
Serial.print(attempt);
Serial.print(", VersionReg=0x");
Serial.println(version, HEX);
// 0x91=v1.0, 0x92=v2.0 为合法0x00/0xFF 表示通讯异常
if (version == 0x91 || version == 0x92) {
rfidReady = true;
break;
}
delay(100); // 重试前等待
}
if (rfidReady) {
Serial.println("RFID initialized.");
} else {
Serial.println("RFID initialization FAILED after 3 attempts! Check wiring/power.");
}
// 初始化LED
FastLED.addLeds<WS2812, LED_PIN_1, GRB>(leds1, LED_COUNT_1);
FastLED.addLeds<WS2812, LED_PIN_2, GRB>(leds2, LED_COUNT_2);
FastLED.addLeds<WS2812, LED_PIN_3, GRB>(leds3, LED_COUNT_3); // 新增LED3
// 启动时先全黑,避免 186 颗 LED 同时点亮产生瞬时 4.5A 大电流
// 冲击 3.3V/5V 电源导致刚初始化好的 RC522 进入异常状态
// TaskLEDUnified 启动后会根据 ledMode/led2Brightness 自动恢复默认显示
fill_solid(leds1, LED_COUNT_1, CRGB::Black);
fill_solid(leds2, LED_COUNT_2, CRGB::Black);
fill_solid(leds3, LED_COUNT_3, CRGB::Black);
FastLED.show();
Serial.println("LED initialized (dark startup, task will restore default).");
// 初始化PWM
// 创建LED控制器配置
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = (ledc_timer_bit_t)PWM_RESOLUTION,
.timer_num = (ledc_timer_t)PWM_CHANNEL,
.freq_hz = PWM_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&ledc_timer);
// 创建LED通道配置
ledc_channel_config_t ledc_channel = {
.gpio_num = PWM_PIN,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = (ledc_channel_t)PWM_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = (ledc_timer_t)PWM_CHANNEL,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&ledc_channel);
// 设置初始占空比
ledc_set_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)PWM_CHANNEL, pwmDuty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)PWM_CHANNEL);
Serial.println("PWM initialized.");
// 初始化输入引脚
pinMode(BTN0_PIN, INPUT_PULLUP);
pinMode(WAKEUP1_PIN, INPUT);
pinMode(BTN1_PIN, INPUT);
pinMode(BTN2_PIN, INPUT);
Serial.println("Inputs initialized.");
// 创建任务
// TaskRFID 放 Core 0避开 Core 1 上 WS2812 bit-banging 关中断窗口SPI 通讯更稳定
xTaskCreatePinnedToCore(
TaskRFIDcode, /* 任务函数 */
"TaskRFID", /* 任务名称 */
4096, /* 任务栈大小 */
NULL, /* 传递给任务的参数 */
2, /* 任务优先级(提高到 2避免被按键任务频繁抢占*/
&TaskRFID, /* 任务句柄 */
0); /* 运行在核心0上与 LED 任务物理隔离)*/
xTaskCreatePinnedToCore(
TaskLEDUnifiedCode,
"TaskLEDUnified",
8192, // 建议栈大一点
NULL,
3, // 提高优先级确保LED更新不被其他任务干扰
NULL,
1);
xTaskCreatePinnedToCore(
TaskPWMcode,
"TaskPWM",
1024,
NULL,
1,
&TaskPWM,
1);
xTaskCreatePinnedToCore(
TaskBTN0code,
"TaskBTN0",
2048,
NULL,
1,
&TaskBTN0,
0);
xTaskCreatePinnedToCore(
TaskWAKEUP1code,
"TaskWAKEUP1",
2048,
NULL,
1,
&TaskWAKEUP1,
0);
xTaskCreatePinnedToCore(
TaskBTN1code,
"TaskBTN1",
2048,
NULL,
1,
&TaskBTN1,
0);
xTaskCreatePinnedToCore(
TaskBTN2code,
"TaskBTN2",
2048,
NULL,
1,
&TaskBTN2,
0);
Serial.println("Tasks created. System ready.");
}
void loop() {
// 同时处理两个串口的命令输入(双端都能下发控制命令)
// - Serial (USB-Serial-JTAG)Windows 调试发命令
// - SerialLinux (UART0 / CH343)Android 开发板发命令
// 两个缓冲区独立保存,避免一方半发命令被另一方打断
static String cmdFromSerial = "";
static String cmdFromLinux = "";
handleCommandFromStream(Serial, cmdFromSerial);
handleCommandFromStream(SerialLinux, cmdFromLinux);
// 让出CPU时间
delay(1);
}

Binary file not shown.

Binary file not shown.