适配 OrangePi CM5 Android 开发板 + 修复 ESP32 冷启动与稳定性问题

一、架构改造
1、双串口架构:Serial (USB-Serial-JTAG, USB2) 用于 Windows 调试日志,
   SerialLinux (UART0/CH343, USB1) 用于 Android 业务数据收发
2、用 #define SerialLinux Serial0 别名引用 Arduino core 自带对象,
   避免自建 HardwareSerial(0) 导致 RX ring buffer 冲突、命令无响应

二、RFID 冷启动稳定性(核心修复)
1、新增 rc522HardResetRuntime():显式拉低拉高 RST 做标准硬件复位时序
2、setup 中 3 次重试初始化 + 读 VersionReg 校验(0x91/0x92 为合法)
3、TaskRFID 运行时每 5 秒健康检查,异常自动恢复
   背景:冷启动 GPIO14 浮空 50% 读到 HIGH,库仅软复位失败 → 刷卡永远无效
   参考 miguelbalboa/rfid Issue #229、#269、#125

三、数据完整性
1、serialPrintlnSafe:互斥锁 + flush 保护业务数据输出
2、卡号格式校验 (HA + 阿拉伯数字),非法数据不发送
3、命令末尾 trim 兼容 \r\n (CRLF) 和 \n (LF) 两种行尾符

四、命令接收(双向打通)
1、processCommand(cmd, Stream& resp):响应回到发送方串口
2、handleCommandFromStream:双串口独立缓冲,Windows 和 Android 都能下发命令
3、新增 RESET 软复位命令(设备封装后无法物理按 RST 时使用)

五、启动稳定性(防硬件冲击与虚假事件)
1、LED 开机全黑启动,避免 186 颗 WS2812 同时点亮产生 4.5A 瞬时电流
   冲击电源导致刚初始化的 RC522 进入异常状态
2、按键任务 lastState 从硬编码改为读取实际 GPIO 电平作为初始值
   避免 GPIO16/17/18 无上下拉浮空触发虚假 SO_WAKEUP/SO_BT 事件

六、性能优化
1、TaskRFID 从 Core 1 迁至 Core 0,与 WS2812 关中断窗口物理隔离
2、LED 刷新频率 30FPS 降至 20FPS,关中断时间减少 33%
3、RFID 认证失败后 delay 从 100ms 降至 30ms,提升刷卡响应速度
4、USB CDC TX 缓冲区扩容至 4KB,降低突发输出时的截断概率

七、新增文件
1、ESP32踩坑经验文档.md:记录 11 个踩坑点 + 修复方案(含 GitHub Issue 佐证)
2、01_HOLOMAIN_旧开发板代码.ino:旧开发板稳定版本代码存档
3、02_HOLOMAIN_香橙派CM5开发板代码.ino:香橙派 CM5 适配版本代码存档
4、docs/ESP32-S3-SCH-V1.4.pdf:ESP32-S3 核心板硬件原理图

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-04-21 16:52:08 +08:00
parent 75662e102e
commit 87926e48a4
5 changed files with 2698 additions and 163 deletions

View File

@ -0,0 +1,869 @@
#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 口),连 Linux 开发板接收业务数据
// UART0 默认引脚TX=GPIO43、RX=GPIO44对应 CH343P 的 RXD/TXD
HardwareSerial SerialLinux(0);
// Serial 输出互斥锁:防止多任务并发写串口导致数据交错/截断
SemaphoreHandle_t serialMutex = NULL;
// 带互斥保护的双串口输出:同时发到 USB CDC (Windows) 和 UART0 (Linux)
// 调用方RFID 任务发 SORC_xxx按键任务发 SO_xxx
// 机制1) 互斥锁避免多任务交错 2) 两个串口并发传输 3) flush 等待传输完成
void serialPrintlnSafe(const String& msg) {
if (serialMutex && xSemaphoreTake(serialMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// ---- 发到 USB CDC (Windows 调试) ----
// 等待 TX 缓冲区有足够空间,避免 USB CDC 缓冲区满导致截断(最多等 50ms
size_t needed = msg.length() + 2;
uint32_t waitStart = millis();
while ((size_t)Serial.availableForWrite() < needed && (millis() - waitStart) < 50) {
vTaskDelay(pdMS_TO_TICKS(2));
}
Serial.println(msg);
Serial.flush();
// ---- 发到 UART0 (Linux 业务) ----
// UART0 硬件有独立 128 字节 FIFO115200 波特率下发送 12 字节约 1ms几乎不阻塞
SerialLinux.println(msg);
SerialLinux.flush();
xSemaphoreGive(serialMutex);
} else {
// 拿不到锁的降级路径(正常情况下几乎不触发)
Serial.println(msg);
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: 白色
};
// RFID读取任务
void TaskRFIDcode(void* pvParameters) {
for (;;) {
// 寻找新卡片
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;
static bool lastState = HIGH;
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) {
static bool lastState = LOW;
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) {
static bool lastState = LOW;
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) {
static bool lastState = LOW;
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);
}
}
// 串口命令处理
void handleSerialCommand() {
static String command = "";
while (Serial.available()) {
// 先检查命令长度(新增的防护代码)
if (command.length() > 64) {
Serial.println("错误: 命令过长最大64字符");
command = ""; // 清空当前命令
while (Serial.available()) Serial.read(); // 清空串口缓冲区
continue;
}
char c = Serial.read();
if (c == '\n') {
// 去掉命令末尾可能的 \r兼容不同行尾符设置\n 或 \r\n
command.trim();
// 处理命令
if (command.startsWith("MO_LED_")) {
String modeStr = command.substring(7);
int newMode = modeStr.toInt();
// 控制单颗LED
if (newMode >= 0 && newMode <= 7) {
singleLedMode = newMode;
Serial.print("Single LED set to mode: ");
Serial.println(newMode);
} else {
Serial.println("Invalid single LED mode command");
}
} else if (command.startsWith("MO_LEDN_")) {
String modeStr = command.substring(8);
int newMode = modeStr.toInt();
// 控制灯带只有亮度不为0时才允许
if (newMode >= 0 && newMode <= 5) {
if (led2Brightness == 0) {
Serial.println("当前亮度为0请先将亮度调整至0以上再切换显示模式");
} else {
ledMode = newMode;
// 重置火车灯状态
if (newMode == 4) {
trainPos = -TRAIN_LENGTH;
trainPhase = 0;
rainbowHue = random8();
}
// 新增切换到模式5时复制当前LED2状态和亮度
if (newMode == 5) {
memcpy(frozenLeds2, leds2, sizeof(leds2));
frozenBrightness = led2Brightness; // 保存冻结时的亮度
}
Serial.print("LED strip set to mode: ");
Serial.println(newMode);
}
} else {
Serial.println("Invalid LED strip mode command");
}
} else if (command.startsWith("MO_PWM_")) {
String dutyStr = command.substring(7);
int newDuty = dutyStr.toInt();
// 检查PWM百分比
if (newDuty == 1) {
pwmDuty = 1023; // 100%
} else if (newDuty == 0 || newDuty == 20 || newDuty == 40 || newDuty == 60 || newDuty == 80) {
pwmDuty = (newDuty * 1023) / 100; // 转换为实际占空比
} else {
Serial.println("Invalid PWM command");
}
Serial.print("PWM set to: ");
Serial.print((pwmDuty * 100) / 1023);
Serial.println("%");
}
else if (command.startsWith("MO_BRI_")) {
// 提取亮度参数(跳过"MO_BRI_"前缀)
String levelStr = command.substring(7);
levelStr.trim();
command = ""; // 清空命令缓冲区
// 空参数检查
if (levelStr.length() == 0) {
Serial.println("错误: 缺少亮度值");
return; // 终止处理
}
// 严格数字验证(拒绝非数字字符)
bool isNumeric = true;
for (char c : levelStr) {
if (!isdigit(c)) {
isNumeric = false;
break; // 发现非数字立即退出
}
}
// 非数字错误处理
if (!isNumeric) {
Serial.println("错误: 亮度值必须为整数");
return;
}
// 转换为整数并验证范围
int level = levelStr.toInt();
if (level >= 0 && level <= 100) {
// 更新亮度值映射到PWM范围
led2Brightness = brightnessMap[level]; // 使用预定义映射表
Serial.print("LED亮度: ");
Serial.print(level);
Serial.println("%");
// // 亮度为0时输出警告
// if (level == 0) {
// Serial.println("亮度已设置为0所有灯光将熄灭");
// }
} else {
Serial.println("错误: 亮度值需在0-100之间");
}
command = ""; // 清空命令
}
// 软复位命令:串口发送 "RESET" 触发 ESP32 重启
else if (command == "RESET") {
Serial.println("System resetting...");
Serial.flush(); // 等待串口数据发送完成
delay(100); // 留时间让串口打印完
ESP.restart(); // 触发软复位
}
command = ""; // 清空命令
} else {
// 累积非换行符字符
command += 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
rfid.PCD_Init();
Serial.println("RFID initialized.");
// 初始化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
// 初始化LED状态
fill_solid(leds1, LED_COUNT_1, singleLedColors[singleLedMode]);
fill_solid(leds2, LED_COUNT_2, CHSV(0, 0, led2Brightness)); // 初始化白色
fill_solid(leds3, LED_COUNT_3, CRGB::Black); // 强制GPIO48的灯珠熄灭
FastLED.show();
Serial.println("LED initialized.");
// 初始化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() {
// 处理串口命令
handleSerialCommand();
// 让出CPU时间
delay(1);
}

View File

@ -0,0 +1,906 @@
#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);
}
}
// 处理单条完整命令
// 参数 resp 指向发送方串口命令响应会回到发送方Windows 发命令 Windows 收响应Android 同理)
void processCommand(const String& command, Stream& resp) {
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);
}

638
ESP32踩坑经验文档.md Normal file
View File

@ -0,0 +1,638 @@
# ESP32-S3 HOLOMAIN 项目踩坑与修复经验
> 记录时间2026-04-21
> 硬件ESP32-S3-WROOM-1-N16R8 + CH343P + OrangePi CM5 (Android)
> 核心场景OrangePi CM5 Android 开发板作为主控 → ESP32 作为外设NFC/LED/按键)
> 项目代码:`HOLOMAIN.ino`
---
## 一、硬件架构
### 板载两个 Type-C 口对应不同 USB 通道
| 标号 | 丝印 | 内部通道 | VID/PID | 连接方向 |
|------|------|---------|--------|---------|
| **USB1** | CH343 口 | CH343P(USB-UART桥) → ESP32 UART0 (GPIO43/44) | `1A86:55D5` | Android (业务/烧录) |
| **USB2** | JTAG 口 | ESP32-S3 原生 USB-Serial-JTAG (GPIO19/20) | `303A:1001` | Windows (调试/烧录) |
### 关键硬件特性(从原理图推断)
- **R11 10kΩ 上拉到 GPIO3**:导致 UART0 启动时进入 Silent Boot只输出 `ESP-ROM:esp32s3-20210327` 一行
- **CH343P 的 DTR/RTS 通过 Q1/Q2 三极管控制 EN 和 GPIO0**:支持 Arduino IDE 从 CH343 口自动进下载模式
- **两个 USB 口都能给板子供电**(经二极管 D1/D2/D3 合路)
- **UART0 固定 GPIO43(TX)/GPIO44(RX)**,对应 CH343P 的 RXD/TXDCH343 工作在 USB-UART 桥模式)
---
## 二、踩坑清单(按问题严重程度排序)
### 坑 1OrangePi CM5 Android 对 ESP32 原生 USB-Serial-JTAG 兼容性差【严重】
**现象**
- 之前用其他 Linux 开发板通过 USB-Serial-JTAG 连 ESP32稳定工作
- 换成 OrangePi CM5 Android 后,偶发 NFC 刷卡不切换形象、命令无响应、数据截断
**根本原因**
- ESP32-S3 USB-Serial-JTAG 是芯片内置的"软" USB CDC 实现
- 不同平台对 `303A:1001` 这个 CDC-ACM 设备的驱动支持差异大:
- Windows标准 CDC 驱动稳定
- 通用 Linux (Ubuntu/Debian)`cdc_acm` 内核模块成熟
- **定制 AndroidRK3588/Rockchip SDK**USB Host 权限管理 + CDC 解析未针对 ESP32-S3 充分测试,数据读取不稳定
**修复方案**
- **把业务数据切到 UART0 + CH343P 路径**
- CH343 作为成熟的 USB-UART 桥芯片VID/PID 是 `1A86:55D5`,所有 Android 通过 `com.hoho.android.usbserial` 库完美支持 CH340/CH343 家族
**经验教训**
> **面向消费级 Android 硬件,永远优先选 CH340/CH343/CP2102 这类老牌 USB-UART 桥芯片,不要依赖 ESP32 自带的 USB-Serial-JTAG 做业务通讯**。自带 USB 只适合给 Windows/通用 Linux 做调试烧录。
---
### 坑 2自创建 `HardwareSerial(0)` 与 Arduino core 的 Serial0 冲突【严重】
**现象**
- 代码里写了 `HardwareSerial SerialLinux(0)` 想独立管理 UART0
- `SerialLinux.println()` 能正常发送Android 能收到 `SORC_HA003`
- 但 `SerialLinux.available()` 始终读不到数据Android 发命令 ESP32 不响应)
**根本原因**
- Arduino-ESP32 core 在启动时**自动创建全局 `Serial0` 对象管理 UART0**
- UART0 RX 中断由 core 统一处理,数据被放进 `Serial0` 的 ring buffer
- 自创建的 `HardwareSerial(0)``Serial0` 底层共享同一硬件,但 ring buffer 状态独立
- 自建对象的 `available()` 永远返回 0
**修复方案**
```cpp
// 错误做法(看似正确,实则接收不到数据):
HardwareSerial SerialLinux(0);
// 正确做法(用宏别名引用 core 对象):
#define SerialLinux Serial0
```
**经验教训**
> **在 Arduino-ESP32 中访问硬件 UART始终用 core 提供的全局对象Serial0/Serial1/Serial2不要自己实例化 `HardwareSerial(n)`**。编译期宏别名 `#define` 是零开销且安全的方案。
---
### 坑 3USB CDC On Boot 配置与 Serial 对象归属【重要】
**现象**
- 代码里 `Serial.println("System starting...")` 在串口监视器上只看到一行 `ESP-ROM:esp32s3-20210327`
- 发 `RESET` 命令无响应
- 应用完全像没启动
**根本原因**
- Arduino IDE 工具菜单有 `USB CDC On Boot` 开关,影响 `Serial` 对象的归属:
| 配置 | Serial 对应 | 调试口 | 业务口(本项目)|
|------|------------|--------|----------------|
| `USB CDC On Boot: Disabled` | UART0 (Serial0) | CH343 口USB1| CH343 口(与调试口争用)|
| `USB CDC On Boot: Enabled` | USB-Serial-JTAG (HWCDC) | JTAG 口USB2| UART0 独立SerialLinux = Serial0|
**本项目选 Enabled**:因为 Windows 调试插 JTAG 口USB2Android 业务插 CH343 口USB1两条路物理隔离。
**补充R11 上拉 GPIO3 的影响**
- 即使 `Serial` 走 USB-Serial-JTAGUART0 默认还会输出 ROM bootloader 启动信息
- 但因 GPIO3 被上拉 → 进入 **Silent Boot** → UART0 只输出芯片型号那一行
- USB-Serial-JTAG 通道的启动日志不受 GPIO3 影响
---
### 坑 4ESP32-S3 没有 RST/BOOT 按钮时如何复位【小坑】
**现象**
- 设备装好后 ESP32 板子藏在结构件里,按不到 RST 按钮
- 烧录完成后想重启观察启动日志,没办法触发
**修复方案**
1. **关闭再打开串口监视器**DTR 控制会自动触发复位)
2. **拔插调试 USB 线**(断电重启)
3. **重新烧录**(烧录完成会自动重启)
4. **加软复位命令**(推荐长期方案):
```cpp
else if (command == "RESET") {
Serial.println("System resetting...");
Serial.flush();
delay(100);
ESP.restart();
}
```
后续通过串口发送 `RESET` 即可触发重启。
---
### 坑 5串口数据截断`SORC_H` 这种不完整数据)【业务致命】
**现象**
- 预期收到 `SORC_HA003\r\n`,实际收到 `SORC_H\r\n`
- Linux/Android 端查表找不到 `SORC_H` 对应的形象 → 表现为"刷卡后形象偶尔不切换"
**根本原因**
- 多个 FreeRTOS 任务同时调用 `Serial.println` 时,两次 print 之间可能被其他任务插入
- USB CDC 内部 FIFO 缓冲区满时,`Serial.flush()` 有超时(约 100ms超时后返回未发送字节被丢弃
- `Serial.availableForWrite()` 在 HWCDC 上返回的是内部 ring buffer 剩余空间,**不等于 USB 硬件 FIFO 剩余空间**
**修复方案**(多层防护):
1. **互斥锁串行化多任务 Serial 写入**
```cpp
SemaphoreHandle_t serialMutex = xSemaphoreCreateMutex();
void serialPrintlnSafe(const String& msg) {
if (xSemaphoreTake(serialMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
SerialLinux.println(msg);
SerialLinux.flush();
xSemaphoreGive(serialMutex);
}
}
```
2. **业务数据走 UART0不走 USB CDC**UART0 硬件 FIFO 128 字节 + 114.2kbps 发送速率稳定,几乎不会截断。
3. **增大 USB CDC TX 缓冲(给调试日志用)**
```cpp
Serial.setTxBufferSize(4096);
Serial.begin(115200);
```
4. **接收端正则过滤兜底**Android/Linux 端):
```python
import re
pattern = re.compile(r'^SORC_HA\d+$')
if pattern.match(line): # 截断数据不匹配,自动丢弃
handle_card(line)
```
**经验教训**
> **跨主机的串口业务必须定义严格的数据格式**(如 `^SORC_HA\d+$`接收端做正则校验这是数据完整性的最后一道保险。ESP32 端再优化也不可能做到 100% 零截断。
---
### 坑 6调试日志也走互斥保护导致刷卡变慢【中等】
**现象**
- 业务数据用 `serialPrintlnSafe`(互斥 + flush保护后很完整
- 但把 `Authentication failed``Reading failed` 这类调试日志也改用 `serialPrintlnSafe` 后,**刷卡响应明显变慢**
**根本原因**
- 每次 RFID 认证失败都要 flush 等 USB 传输完成(可能阻塞 50ms+
- WS2812 干扰导致失败率本身就不低(~10%
- 连续 3 次失败 × 100ms = 累积 300-400ms 阻塞 → 肉眼可见的"不灵敏"
**修复方案**
- **业务数据**`SORC_HAxxx``SO_BTx`)→ `serialPrintlnSafe`(互斥 + flush确保完整
- **调试日志**`Authentication failed`)→ 普通 `Serial.println`(快速返回,偶尔截断由接收端过滤)
- **失败后 delay 从 100ms 降到 30ms**RC522 实际恢复 10-20ms 就够
**经验教训**
> **分清业务数据和调试日志的优先级**。业务数据要"完整准确"(可接受稍慢),调试日志要"不阻塞"(可接受偶尔截断)。全部用最严格的保护会拖垮性能。
---
### 坑 7WS2812 bit-banging 关中断干扰 SPI 通讯【硬件干扰】
**现象**
- 灯带满亮度 + 彩虹流水动画时,刷卡失败率明显升高
- 日志出现 `Authentication failed: Error in communication``Reading failed: The CRC_A does not match`
**根本原因**
- FastLED 驱动 WS2812 是通过 GPIO bit-banging 产生精确时序(每位 1.25μs
- 186 颗灯 × 24 位 × 1.25μs ≈ **5.6ms 关中断窗口** × 每秒 30 次 = 168ms/秒关中断
- 关中断期间 ESP32 无法响应 SPI 中断 → RC522 通讯受影响
**缓解方案**(代码层面):
1. **RFID 任务迁移到 Core 0**,与 LED 任务Core 1物理隔离
```cpp
xTaskCreatePinnedToCore(TaskRFIDcode, "TaskRFID", 4096, NULL, 2, &TaskRFID, 0);
xTaskCreatePinnedToCore(TaskLEDUnifiedCode, "TaskLEDUnified", 8192, NULL, 3, NULL, 1);
```
2. **LED 刷新从 30FPS 降到 20FPS**:减少 33% 关中断窗口
```cpp
const unsigned long LED_UPDATE_INTERVAL = 50; // 原 33
```
**根本解决方案**(非本项目采用,留作参考):
- 改用 ESP32-S3 的 **RMT 硬件外设驱动 WS2812**(不需要 CPU 关中断)
- FastLED 的 `#define FASTLED_ESP32_I2S` 或用 ESP-IDF 的 `led_strip` 组件
- 硬件电路给 RC522 的 3.3V 加去耦电容100μF + 10μF + 100nF 组合)
**经验教训**
> **WS2812 bit-banging 与高速 SPI/I2S 同时使用时必然有干扰**。RFID 这种对通讯时序敏感的外设应尽量分到不同 Core并接受 5-10% 的偶发失败率是正常的。
---
### 坑 8行尾符不一致导致命令不被识别【低级错误高频踩】
**现象**
- Windows 串口监视器发 `MO_LED_4` 能生效
- Android 发同样命令 ESP32 没反应
**根本原因**
- Android 发的是 `MO_LED_4\r\n`CRLF 行尾)
- 旧代码的 `command == "RESET"` 等精确匹配会失败(因为 command 实际是 `RESET\r`
- `command.startsWith(...)` 型的判断不受影响(因为 `substring(7)` 后的 `toInt()` 能忽略 `\r`
- 特殊校验(如 `MO_BRI_` 的"必须全是数字")会因 `\r` 被误判为非数字
**修复方案**
```cpp
if (c == '\n') {
command.trim(); // 去掉末尾 \r兼容 CRLF 和 LF
if (command.length() > 0) {
processCommand(command, src);
}
command = "";
}
```
**Arduino `println()` 默认已经是 `\r\n`**,所以 ESP32 发出去的数据天然兼容两种平台。只需保证接收端做 trim 即可。
---
### 坑 9`handleSerialCommand` 只读 Serial 导致 Android 命令无响应【架构 bug】
**现象**
- Android 发命令 ESP32 不响应
- Windows 发同样命令正常
**根本原因**
```cpp
void handleSerialCommand() {
while (Serial.available()) { ... } // 只读 SerialUSB CDC
}
```
Android 的命令走 UART0 → Serial0 ring buffer但代码里 `Serial0.available()` 从未被调用。
**修复方案**(函数参数化 + 双缓冲区):
```cpp
void processCommand(const String& command, Stream& resp) {
// 统一的命令处理逻辑,响应回到发送方
}
void handleCommandFromStream(Stream& src, String& cmdBuf) {
while (src.available()) {
char c = src.read();
if (c == '\n') {
cmdBuf.trim();
if (cmdBuf.length() > 0) processCommand(cmdBuf, src);
cmdBuf = "";
} else {
cmdBuf += c;
}
}
}
void loop() {
static String cmdFromSerial = ""; // Windows/USB CDC 独立缓冲
static String cmdFromLinux = ""; // Android/UART0 独立缓冲
handleCommandFromStream(Serial, cmdFromSerial);
handleCommandFromStream(SerialLinux, cmdFromLinux);
delay(1);
}
```
**经验教训**
> **每个串口必须有独立的命令缓冲区**,否则一方半发命令可能被另一方打断。`Stream&` 多态 + 缓冲区参数化是最优雅的写法。
---
### 坑 10NFC 同卡连续刷卡的去重策略【业务逻辑】
**演进过程**
1. **最初**`if (cardData != lastCardData)` → 同卡永久不再发送
- 问题Linux 异常清屏后无法恢复形象
2. **尝试 1**30 秒时间窗口去重
- 问题过于保守1 小时后刷同卡反而触发刷新(实际 Linux 状态没变)
3. **尝试 2**:完全不去重,每次都发
- 问题用户快速重复刷同卡Android 频繁刷屏
4. **最终**:根据业务需求决定
- 项目选择:**完全不去重**Linux 端自己根据需要判断是否重复响应)
- 或者 3 秒时间窗口兜底(防手抖)
**经验教训**
> **去重逻辑本质是业务决策**,代码层面先提供最简单的"每次都发",把去重权交给业务层。数据完整性(不截断、不丢包)才是嵌入式固件的核心职责。
---
### 坑 11RC522 冷启动刷卡无效软复位ESP.restart后就正常【高频坑经典】
**现象**
- **断电重启**后LED 能控制,但刷 NFC 卡完全无响应
- **串口发 RESET 触发 ESP.restart()**NFC 又能正常工作了
- 问题可复现,但不是 100%(有时候冷启动也正常,有时候失败)
**根本原因**
`RFID_RST_PIN 14` 虽然在代码里定义了,但**只传给了 `MFRC522 rfid(SS, RST)` 构造函数,没有任何 `pinMode`/`digitalWrite` 显式操作**。真正的复位逻辑由 `rfid.PCD_Init()` 内部处理。
查阅 miguelbalboa/rfid 库源码:
```cpp
void MFRC522::PCD_Init() {
pinMode(_resetPowerDownPin, INPUT); // 第 1 步RST 设为 INPUT
if (digitalRead(_resetPowerDownPin) == LOW) {
// 读到 LOW → 硬件复位(拉高 RST + delay 50ms
hardReset = true;
}
if (!hardReset) {
PCD_Reset(); // 读到 HIGH → 只做软件复位(写 CommandReg
}
}
```
**冷启动的失败流程**
1. ESP32 上电GPIO14 默认 INPUT 浮空 → 电平随机HIGH 或 LOW
2. 同时 RC522 模组独立上电,内部 POR 需要 10-50ms 稳定
3. Arduino setup 立刻调用 PCD_Init
4. 库读 GPIO14 → **恰好是 HIGH 的概率 50%+** → 只做软件复位
5. 此时 RC522 内部可能还没完成 POR → 软件复位的寄存器操作失败
6. RC522 进入"半初始化"状态:能回应 ReadCardSerial但 Authenticate 永远失败
7. 用户看到LED 正常、刷卡不响应
**软复位ESP.restart为什么有效**
- 前一次 `PCD_Init()` 执行硬件复位分支时,已经把 GPIO14 设为 OUTPUT + HIGH
- `ESP.restart()` 触发时GPIO 状态保留几毫秒(电容效应)
- **关键**:此时 RC522 硬件已经工作正常(它没断电)
- 重启后 PCD_Init 读 GPIO14 → 稳定读到 HIGH → 做软件复位 → 成功
**修复方案**(三层保险):
1. **全局工具函数:标准硬件复位时序**
```cpp
void rc522HardResetRuntime() {
pinMode(RFID_RST_PIN, OUTPUT);
digitalWrite(RFID_RST_PIN, LOW); // 拉低触发硬件复位
delay(10); // 规格 ≥100ns10ms 绝对安全
digitalWrite(RFID_RST_PIN, HIGH); // 释放 RST
delay(50); // 等待晶振起振 + POR
}
```
2. **setup() 强制 3 次重试 + 版本校验**
```cpp
bool rfidReady = false;
for (uint8_t attempt = 1; attempt <= 3; attempt++) {
rc522HardResetRuntime();
rfid.PCD_Init();
byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
// 0x91=v1.0, 0x92=v2.0 合法0x00/0xFF 异常
if (version == 0x91 || version == 0x92) { rfidReady = true; break; }
delay(100);
}
```
3. **运行时健康检查(每 5 秒)**
```cpp
// 防止运行过程中 RC522 因电源波动、WS2812 大电流冲击进入异常
static uint32_t lastHealthCheck = 0;
if (millis() - lastHealthCheck > 5000) {
lastHealthCheck = millis();
byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
if (version != 0x91 && version != 0x92) {
rc522HardResetRuntime();
rfid.PCD_Init();
}
}
```
**GitHub 相关 Issue 佐证**
- miguelbalboa/rfid Issue #229 - ESP32 cold boot authentication failed
- miguelbalboa/rfid Issue #269 - PCD_Init works but Authenticate fails first time
- miguelbalboa/rfid Issue #125 - Random first-read failures on ESP32
**经验教训**
> **任何带 RST 引脚的外设芯片RC522/LCD/音频芯片等setup 必须显式做标准硬件复位时序**,不要依赖外设库的"自动判断"。冷启动时 GPIO 浮空是万恶之源,库的启发式判断在 ESP32 这种高速启动场景下不可靠。
>
> **校验初始化结果**:初始化完成后读一个"已知固定值"的寄存器如版本号能确认芯片真的活了。MFRC522 就是读 `VersionReg`0x91/0x92
**验证方法**
- 断电 → 10 秒 → 上电 → 立刻刷卡,重复 10 次应该全成功
- 观察串口输出 `RC522 init attempt 1, VersionReg=0x92`
- 如果偶尔看到 `attempt 2/3`,说明重试机制被触发了,但最终成功
---
## 三、最终架构
```
+----------------------+
| Windows 开发电脑 |
+----------+-----------+
|
| USB Type-C
v
+------------------------------------+
| USB2 (USB-Serial-JTAG) |
| → ESP32-S3 原生 USB (GPIO19/20) |
+------------------------------------+
|
Serial 对象 (HWCDC)
|
v
+-----------------------------------------------+
| ESP32-S3-WROOM-1 |
| |
| RFID(SPI) ← TaskRFID @ Core 0 |
| |
| WS2812 ← TaskLEDUnified @ Core 1 (20FPS) |
| |
| Button × 4 ← TaskBTN × 4 @ Core 0 |
| |
| SerialLinux (=Serial0) 对象 |
| |
+-----------------------------------------------+
^
|
UART0 (GPIO43/44)
|
v
+------------------------------------+
| USB1 (CH343P USB-UART 桥) |
+------------------------------------+
|
| USB Type-C
v
+----------------------+
| OrangePi CM5 (Android) |
| SPUP 通过 USB Host API |
| 匹配 VID/PID 1A86:55D5 |
+----------------------+
```
### 数据流向矩阵
| 数据 | 发送方 | 通道 | 接收方 |
|------|-------|------|--------|
| `SORC_HAxxx` (刷卡事件) | RFID 任务 | UART0/CH343 | Android |
| `SO_BTx_HIGH/LOW/HIGHL` (按键事件) | 按键任务 | UART0/CH343 | Android |
| `SO_WAKEUP0/1` (唤醒事件) | 唤醒任务 | UART0/CH343 | Android |
| `Authentication failed` (调试日志) | RFID 任务 | USB CDC | Windows |
| `Reading failed` (调试日志) | RFID 任务 | USB CDC | Windows |
| `System starting...` (启动日志) | setup() | 两边都发 | 两边 |
| `MO_LED_*` / `MO_LEDN_*` / `MO_BRI_*` / `MO_PWM_*` / `RESET` (控制命令) | Windows 或 Android | 任一 Stream | ESP32 处理 |
| 命令响应(如 `LED strip set to mode: 2` | ESP32 | 回到发送方 | Windows 或 Android |
---
## 四、速查表Arduino IDE 关键配置
| 菜单项 | 本项目值 | 说明 |
|-------|---------|------|
| 开发板 | ESP32S3 Dev Module | ESP32-S3-WROOM-1-N16R8 |
| USB CDC On Boot | **Enabled** | Serial 走 USB-Serial-JTAGUART0 通过 Serial0 访问 |
| USB DFU On Boot | Disabled | 不使用 DFU 模式 |
| USB Mode | Hardware CDC and JTAG | 使能 USB-Serial-JTAG 功能 |
| Flash Size | **16MB (128Mb)** | N16R8 模组实际 Flash |
| Partition Scheme | 按需选 | 代码小可选默认 4MB |
| PSRAM | Disabled | 本项目未使用 PSRAM |
| Upload Mode | UART0 / Hardware CDC | 烧录走哪个口取决于选择 |
| Upload Speed | 921600 | 烧录速度 |
| 端口 | COM8 (USB-Serial-JTAG) | Windows 端看到的 JTAG 口 |
### 串口监视器设置
- 波特率:**115200**
- 行尾符:**换行**(或 "换行 和 回车 两者都是",代码已兼容)
---
## 五、调试技巧总结
### 1. 区分 COM 口是哪个 USB 通道
- 拔插 USB 线,观察 Windows 设备管理器哪个 COM 口消失
- `USB JTAG/serial debug unit` → USB-Serial-JTAG (VID/PID `303A:1001`)
- `USB-Enhanced-SERIAL CH343` → CH343P (VID/PID `1A86:55D5`)
### 2. 没 RST 按钮时如何复位
- 关闭再打开串口监视器DTR 自动触发)
- 发送 `RESET` 命令(代码里的软复位)
- 拔插 USB
### 3. 快速判断 ROM 日志异常
- ESP32-S3 正常启动应输出 7-8 行 ROM 日志(`Build:``rst:``mode:``load:``entry` 等)
- 只输出一行 `ESP-ROM:esp32s3-...` → 可能 **Silent Boot**GPIO3 上拉)或 **输出口错**
### 4. 判断是截断还是根本没发送
- 完整的 `SORC_HA003` → ESP32 发送成功,主机接收正常
- `SORC_H` / `SORC_HA00`(不完整)→ 发送过程中截断
- 完全没有 → ESP32 没发或者链路断开
### 5. 验证 UART0 物理连通
- ESP32 代码加一句 `Serial0.println("TEST")`
- Android 端看能否收到 `TEST`
- 能收到 → UART0 物理链路 OK
- 收不到 → 波特率/接线/CH343 驱动问题
---
## 六、最重要的 3 条经验
### 1. 面向 Android 业务通讯选择 USB-UART 桥芯片CH340/CH343不要用 ESP32 自带 USB-Serial-JTAG
**原因**:定制 AndroidRK3588 等)的 USB CDC 驱动对 ESP32-S3 支持差,`com.hoho.android.usbserial` 库对 CH340 家族有 10+ 年成熟支持。
### 2. 访问硬件 UART 必须用 Arduino core 自带的 Serial0/Serial1/Serial2
**原因**:自建 `HardwareSerial(n)` 对象和 core 的全局对象共享硬件但 ring buffer 独立,**收不到数据**。用 `#define` 别名既清晰又安全。
### 3. 跨主机串口业务定义严格数据格式,接收端做正则兜底
**原因**ESP32 端所有软件优化互斥、flush、缓冲都不能做到 100% 零截断。接收端用 `^SORC_HA\d+$` 过滤是数据完整性最可靠的保险。
---
## 七、关于 OrangePi CM5 + ESP32 的专项建议
1. **业务数据走 UART0 → CH343P → USB1**,不要走 USB-Serial-JTAG
2. **Android APP 端 SPUP 配置**
- VID = `0x1A86`WCH
- PID = `0x55D5`CH343具体看芯片版本
- 波特率 = 115200
- 行尾符 = `\r\n`CRLF
3. **接收端正则过滤**`^SORC_HA\d+$``^SO_BT[0-9]_(HIGH|LOW|HIGHL)$``^SO_WAKEUP[01]$`
4. **调试时 Windows 插 USB2JTAG 口)**,不影响 Android 的 USB1 业务
5. **两根 USB 线同时连接**是安全的(两条路径独立,不抢总线)
---
## 八、未解决/待改进项
### 1. WS2812 bit-banging 与 SPI 的根本冲突
- 当前靠 Core 隔离 + 降 FPS 缓解,仍有 5-10% 刷卡失败率
- 彻底解决方案:改用 RMT 硬件驱动 WS2812需改 FastLED 配置)
### 2. Android 端 SPUP 的 VID/PID 匹配
- 如果 Android 端 APP 被更新为用 ESP32 自带 USB`303A:1001`),所有架构分析失效
- 建议和 Android 开发者锁定**永远用 CH343 作为业务口**
### 3. 双主机冲突防护
- 目前假设 Windows 只在调试时插
- 如果生产环境 Windows 意外插入 USB2可能干扰启动USB 枚举影响 Strapping 引脚状态)
- 建议生产设备完全封死 USB2 口
---
## 附录:关键代码片段
### `serialPrintlnSafe` (业务数据安全输出)
```cpp
SemaphoreHandle_t serialMutex = NULL;
void serialPrintlnSafe(const String& msg) {
if (serialMutex && xSemaphoreTake(serialMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
SerialLinux.println(msg);
SerialLinux.flush();
xSemaphoreGive(serialMutex);
} else {
SerialLinux.println(msg); // 降级路径
}
}
```
### `handleCommandFromStream` (双串口命令接收)
```cpp
void handleCommandFromStream(Stream& src, String& cmdBuf) {
while (src.available()) {
if (cmdBuf.length() > 64) { // 防御性长度保护
src.println("错误: 命令过长");
cmdBuf = "";
while (src.available()) src.read();
return;
}
char c = src.read();
if (c == '\n') {
cmdBuf.trim(); // 兼容 \r\n
if (cmdBuf.length() > 0) processCommand(cmdBuf, src);
cmdBuf = "";
} else {
cmdBuf += c;
}
}
}
```
### UART0 正确声明
```cpp
// 推荐:用宏别名(零开销 + 安全)
#define SerialLinux Serial0
// 不推荐自创建对象available 读不到数据)
// HardwareSerial SerialLinux(0); // 错误写法
// setup() 里初始化
SerialLinux.begin(115200);
```
### 软复位命令
```cpp
else if (command == "RESET") {
Serial.println("System resetting...");
SerialLinux.println("System resetting...");
Serial.flush();
SerialLinux.flush();
delay(100);
ESP.restart();
}
```

View File

@ -35,6 +35,37 @@
// 任务句柄
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颗灯珠数组
@ -107,9 +138,39 @@ CRGB singleLedColors[8] = {
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);
@ -133,11 +194,12 @@ void TaskRFIDcode(void* pvParameters) {
MFRC522::StatusCode status;
status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &(rfid.uid));
if (status != MFRC522::STATUS_OK) {
Serial.print(F("Authentication failed: "));
Serial.println(rfid.GetStatusCodeName(status));
// 调试日志用普通 Serial.println失败频率较高时避免 flush 阻塞拖慢 RFID 响应
// 偶尔截断可接受Linux 端用正则 ^SORC_HA\d+$ 过滤业务数据即可)
Serial.println(String("Authentication failed: ") + rfid.GetStatusCodeName(status));
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
delay(100);
delay(30); // 从 100ms 降到 30ms提升刷卡响应速度
continue;
}
@ -146,23 +208,47 @@ void TaskRFIDcode(void* pvParameters) {
byte size = sizeof(buffer);
status = rfid.MIFARE_Read(4, buffer, &size);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("Reading failed: "));
Serial.println(rfid.GetStatusCodeName(status));
} else {
// 转换为ASCII字符串
for (byte i = 0; i < 16; i++) {
if (buffer[i] >= 32 && buffer[i] <= 126) { // 可打印ASCII字符
cardData += (char)buffer[i];
}
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();
// 移除空白字符
cardData.trim();
// 卡片数据处理
if (cardData != lastCardData && !cardData.isEmpty()) {
lastCardData = cardData;
Serial.println("SORC_" + cardData);
// 卡片数据格式校验:规则 "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;
}
}
@ -207,7 +293,9 @@ void TaskPWMcode(void* pvParameters) {
// 按钮0检测任务
void TaskBTN0code(void* pvParameters) {
static unsigned long pressStartTime = 0;
static bool lastState = HIGH;
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN0_PIN);
btn0State = lastState;
for (;;) {
bool currentState = digitalRead(BTN0_PIN);
@ -216,19 +304,19 @@ void TaskBTN0code(void* pvParameters) {
if (lastState == HIGH && currentState == LOW) {
pressStartTime = millis();
btn0State = LOW;
Serial.println("SO_BT0_HIGH");
serialPrintlnSafe("SO_BT0_HIGH");
btn0LongPress = false;
}
// 检测上升沿(释放)
else if (lastState == LOW && currentState == HIGH) {
btn0State = HIGH;
Serial.println("SO_BT0_LOW");
serialPrintlnSafe("SO_BT0_LOW");
btn0LongPress = false;
}
// 检测长按
else if (currentState == LOW && millis() - pressStartTime >= 2000 && !btn0LongPress) {
btn0LongPress = true;
Serial.println("SO_BT0_HIGHL");
serialPrintlnSafe("SO_BT0_HIGHL");
}
lastState = currentState;
@ -238,7 +326,9 @@ void TaskBTN0code(void* pvParameters) {
// WAKEUP1检测任务
void TaskWAKEUP1code(void* pvParameters) {
static bool lastState = LOW;
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(WAKEUP1_PIN);
wakeup1State = lastState;
for (;;) {
bool currentState = digitalRead(WAKEUP1_PIN);
@ -246,12 +336,12 @@ void TaskWAKEUP1code(void* pvParameters) {
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
wakeup1State = HIGH;
Serial.println("SO_WAKEUP1");
serialPrintlnSafe("SO_WAKEUP1");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
wakeup1State = LOW;
Serial.println("SO_WAKEUP0");
serialPrintlnSafe("SO_WAKEUP0");
}
lastState = currentState;
@ -261,7 +351,9 @@ void TaskWAKEUP1code(void* pvParameters) {
// 按钮1检测任务
void TaskBTN1code(void* pvParameters) {
static bool lastState = LOW;
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN1_PIN);
btn1State = lastState;
for (;;) {
bool currentState = digitalRead(BTN1_PIN);
@ -269,12 +361,12 @@ void TaskBTN1code(void* pvParameters) {
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
btn1State = HIGH;
Serial.println("SO_BT1_HIGH");
serialPrintlnSafe("SO_BT1_HIGH");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
btn1State = LOW;
Serial.println("SO_BT1_LOW");
serialPrintlnSafe("SO_BT1_LOW");
}
lastState = currentState;
@ -284,7 +376,9 @@ void TaskBTN1code(void* pvParameters) {
// 按钮2检测任务
void TaskBTN2code(void* pvParameters) {
static bool lastState = LOW;
// 首次进入任务时读取实际电平作为初始值,避免上电时 GPIO 浮空触发虚假边沿事件
bool lastState = digitalRead(BTN2_PIN);
btn2State = lastState;
for (;;) {
bool currentState = digitalRead(BTN2_PIN);
@ -292,12 +386,12 @@ void TaskBTN2code(void* pvParameters) {
// 检测上升沿
if (lastState == LOW && currentState == HIGH) {
btn2State = HIGH;
Serial.println("SO_BT2_HIGH");
serialPrintlnSafe("SO_BT2_HIGH");
}
// 检测下降沿
else if (lastState == HIGH && currentState == LOW) {
btn2State = LOW;
Serial.println("SO_BT2_LOW");
serialPrintlnSafe("SO_BT2_LOW");
}
lastState = currentState;
@ -305,131 +399,111 @@ void TaskBTN2code(void* pvParameters) {
}
}
// 串口命令处理
void handleSerialCommand() {
static String command = "";
// 处理单条完整命令
// 参数 resp 指向发送方串口命令响应会回到发送方Windows 发命令 Windows 收响应Android 同理)
void processCommand(const String& command, Stream& resp) {
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();
}
}
while (Serial.available()) {
// 先检查命令长度(新增的防护代码)
if (command.length() > 64) {
Serial.println("错误: 命令过长最大64字符");
command = ""; // 清空当前命令
while (Serial.available()) Serial.read(); // 清空串口缓冲区
continue;
// 从指定流读取命令字节,累积到完整一行后交给 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 = Serial.read();
char c = src.read();
if (c == '\n') {
// 处理命令
if (command.startsWith("MO_LED_")) {
String modeStr = command.substring(7);
int newMode = modeStr.toInt();
// 控制单颗LED
if (newMode >= 0 && newMode <= 7) {
singleLedMode = newMode;
Serial.print("Single LED set to mode: ");
Serial.println(newMode);
} else {
Serial.println("Invalid single LED mode command");
}
} else if (command.startsWith("MO_LEDN_")) {
String modeStr = command.substring(8);
int newMode = modeStr.toInt();
// 控制灯带只有亮度不为0时才允许
if (newMode >= 0 && newMode <= 5) {
if (led2Brightness == 0) {
Serial.println("当前亮度为0请先将亮度调整至0以上再切换显示模式");
} else {
ledMode = newMode;
// 重置火车灯状态
if (newMode == 4) {
trainPos = -TRAIN_LENGTH;
trainPhase = 0;
rainbowHue = random8();
}
// 新增切换到模式5时复制当前LED2状态和亮度
if (newMode == 5) {
memcpy(frozenLeds2, leds2, sizeof(leds2));
frozenBrightness = led2Brightness; // 保存冻结时的亮度
}
Serial.print("LED strip set to mode: ");
Serial.println(newMode);
}
} else {
Serial.println("Invalid LED strip mode command");
}
} else if (command.startsWith("MO_PWM_")) {
String dutyStr = command.substring(7);
int newDuty = dutyStr.toInt();
// 检查PWM百分比
if (newDuty == 1) {
pwmDuty = 1023; // 100%
} else if (newDuty == 0 || newDuty == 20 || newDuty == 40 || newDuty == 60 || newDuty == 80) {
pwmDuty = (newDuty * 1023) / 100; // 转换为实际占空比
} else {
Serial.println("Invalid PWM command");
}
Serial.print("PWM set to: ");
Serial.print((pwmDuty * 100) / 1023);
Serial.println("%");
cmdBuf.trim(); // 去掉末尾 \r 兼容不同行尾符
if (cmdBuf.length() > 0) {
processCommand(cmdBuf, src);
}
else if (command.startsWith("MO_BRI_")) {
// 提取亮度参数(跳过"MO_BRI_"前缀)
String levelStr = command.substring(7);
levelStr.trim();
command = ""; // 清空命令缓冲区
// 空参数检查
if (levelStr.length() == 0) {
Serial.println("错误: 缺少亮度值");
return; // 终止处理
}
// 严格数字验证(拒绝非数字字符)
bool isNumeric = true;
for (char c : levelStr) {
if (!isdigit(c)) {
isNumeric = false;
break; // 发现非数字立即退出
}
}
// 非数字错误处理
if (!isNumeric) {
Serial.println("错误: 亮度值必须为整数");
return;
}
// 转换为整数并验证范围
int level = levelStr.toInt();
if (level >= 0 && level <= 100) {
// 更新亮度值映射到PWM范围
led2Brightness = brightnessMap[level]; // 使用预定义映射表
Serial.print("LED亮度: ");
Serial.print(level);
Serial.println("%");
// // 亮度为0时输出警告
// if (level == 0) {
// Serial.println("亮度已设置为0所有灯光将熄灭");
// }
} else {
Serial.println("错误: 亮度值需在0-100之间");
}
command = ""; // 清空命令
}
command = ""; // 清空命令
cmdBuf = "";
} else {
// 累积非换行符字符
command += c;
cmdBuf += c;
}
}
}
@ -447,7 +521,9 @@ void handleSerialCommand() {
// 5. 防闪烁机制确保LED显示稳定
void TaskLEDUnifiedCode(void* pvParameters) {
static unsigned long lastLEDUpdate = 0;
const unsigned long LED_UPDATE_INTERVAL = 33; // ~30FPS降低更新频率减少闪烁
// 20FPS 更新频率:兼顾视觉流畅度和 RFID 稳定性
// 每秒 20 次 WS2812 传输(每次 ~5.6ms 关中断),相比 30FPS 减少 33% 干扰窗口
const unsigned long LED_UPDATE_INTERVAL = 50;
for (;;) {
unsigned long currentTime = millis();
@ -649,28 +725,67 @@ void TaskLEDUnifiedCode(void* pvParameters) {
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
rfid.PCD_Init();
Serial.println("RFID initialized.");
// 初始化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
// 初始化LED状态
fill_solid(leds1, LED_COUNT_1, singleLedColors[singleLedMode]);
fill_solid(leds2, LED_COUNT_2, CHSV(0, 0, led2Brightness)); // 初始化白色
fill_solid(leds3, LED_COUNT_3, CRGB::Black); // 强制GPIO48的灯珠熄灭
// 启动时先全黑,避免 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.");
Serial.println("LED initialized (dark startup, task will restore default).");
// 初始化PWM
// 创建LED控制器配置
@ -709,14 +824,15 @@ void setup() {
Serial.println("Inputs initialized.");
// 创建任务
// TaskRFID 放 Core 0避开 Core 1 上 WS2812 bit-banging 关中断窗口SPI 通讯更稳定
xTaskCreatePinnedToCore(
TaskRFIDcode, /* 任务函数 */
"TaskRFID", /* 任务名称 */
4096, /* 任务栈大小 */
NULL, /* 传递给任务的参数 */
1, /* 任务优先级 */
2, /* 任务优先级(提高到 2避免被按键任务频繁抢占*/
&TaskRFID, /* 任务句柄 */
1); /* 运行在核心1上 */
0); /* 运行在核心0上与 LED 任务物理隔离)*/
xTaskCreatePinnedToCore(
TaskLEDUnifiedCode,
@ -776,8 +892,14 @@ void setup() {
}
void loop() {
// 处理串口命令
handleSerialCommand();
// 同时处理两个串口的命令输入(双端都能下发控制命令)
// - Serial (USB-Serial-JTAG)Windows 调试发命令
// - SerialLinux (UART0 / CH343)Android 开发板发命令
// 两个缓冲区独立保存,避免一方半发命令被另一方打断
static String cmdFromSerial = "";
static String cmdFromLinux = "";
handleCommandFromStream(Serial, cmdFromSerial);
handleCommandFromStream(SerialLinux, cmdFromLinux);
// 让出CPU时间
delay(1);

BIN
docs/ESP32-S3-SCH-V1.4.pdf Normal file

Binary file not shown.