LuoTianyi_HOLOMAIN/02_HOLOMAIN_香橙派CM5开发板代码.ino
Rdzleo 87926e48a4 适配 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>
2026-04-21 16:52:08 +08:00

906 lines
31 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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);
}