diff --git a/01_HOLOMAIN_旧开发板代码.ino b/01_HOLOMAIN_旧开发板代码.ino new file mode 100644 index 0000000..2c786b0 --- /dev/null +++ b/01_HOLOMAIN_旧开发板代码.ino @@ -0,0 +1,869 @@ +#include +#include +#include +#include +#include + +// 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 字节 FIFO,115200 波特率下发送 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); + + // 初始化 UART0(CH343/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(leds1, LED_COUNT_1); + FastLED.addLeds(leds2, LED_COUNT_2); + FastLED.addLeds(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); +} \ No newline at end of file diff --git a/02_HOLOMAIN_香橙派CM5开发板代码.ino b/02_HOLOMAIN_香橙派CM5开发板代码.ino new file mode 100644 index 0000000..a44bf10 --- /dev/null +++ b/02_HOLOMAIN_香橙派CM5开发板代码.ino @@ -0,0 +1,906 @@ +#include +#include +#include +#include +#include + +// 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. 生产场景只连 Android,Windows 不连,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); + + // 初始化 UART0(CH343/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(leds1, LED_COUNT_1); + FastLED.addLeds(leds2, LED_COUNT_2); + FastLED.addLeds(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); +} \ No newline at end of file diff --git a/ESP32踩坑经验文档.md b/ESP32踩坑经验文档.md new file mode 100644 index 0000000..39e4d85 --- /dev/null +++ b/ESP32踩坑经验文档.md @@ -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/TXD(CH343 工作在 USB-UART 桥模式) + +--- + +## 二、踩坑清单(按问题严重程度排序) + +### 坑 1:OrangePi 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` 内核模块成熟 + - **定制 Android(RK3588/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` 是零开销且安全的方案。 + +--- + +### 坑 3:USB 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 口(USB2),Android 业务插 CH343 口(USB1),两条路物理隔离。 + +**补充:R11 上拉 GPIO3 的影响**: +- 即使 `Serial` 走 USB-Serial-JTAG,UART0 默认还会输出 ROM bootloader 启动信息 +- 但因 GPIO3 被上拉 → 进入 **Silent Boot** → UART0 只输出芯片型号那一行 +- USB-Serial-JTAG 通道的启动日志不受 GPIO3 影响 + +--- + +### 坑 4:ESP32-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 就够 + +**经验教训**: +> **分清业务数据和调试日志的优先级**。业务数据要"完整准确"(可接受稍慢),调试日志要"不阻塞"(可接受偶尔截断)。全部用最严格的保护会拖垮性能。 + +--- + +### 坑 7:WS2812 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()) { ... } // 只读 Serial(USB 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&` 多态 + 缓冲区参数化是最优雅的写法。 + +--- + +### 坑 10:NFC 同卡连续刷卡的去重策略【业务逻辑】 + +**演进过程**: +1. **最初**:`if (cardData != lastCardData)` → 同卡永久不再发送 + - 问题:Linux 异常清屏后无法恢复形象 +2. **尝试 1**:30 秒时间窗口去重 + - 问题:过于保守,1 小时后刷同卡反而触发刷新(实际 Linux 状态没变) +3. **尝试 2**:完全不去重,每次都发 + - 问题:用户快速重复刷同卡,Android 频繁刷屏 +4. **最终**:根据业务需求决定 + - 项目选择:**完全不去重**(Linux 端自己根据需要判断是否重复响应) + - 或者 3 秒时间窗口兜底(防手抖) + +**经验教训**: +> **去重逻辑本质是业务决策**,代码层面先提供最简单的"每次都发",把去重权交给业务层。数据完整性(不截断、不丢包)才是嵌入式固件的核心职责。 + +--- + +### 坑 11:RC522 冷启动刷卡无效,软复位(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); // 规格 ≥100ns,10ms 绝对安全 + 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-JTAG,UART0 通过 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 + +**原因**:定制 Android(RK3588 等)的 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 插 USB2(JTAG 口)**,不影响 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(); +} +``` diff --git a/HOLOMAIN.ino b/HOLOMAIN.ino index 6578fcf..a44bf10 100644 --- a/HOLOMAIN.ino +++ b/HOLOMAIN.ino @@ -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. 生产场景只连 Android,Windows 不连,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); + + // 初始化 UART0(CH343/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(leds1, LED_COUNT_1); FastLED.addLeds(leds2, LED_COUNT_2); FastLED.addLeds(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); diff --git a/docs/ESP32-S3-SCH-V1.4.pdf b/docs/ESP32-S3-SCH-V1.4.pdf new file mode 100644 index 0000000..d78b995 Binary files /dev/null and b/docs/ESP32-S3-SCH-V1.4.pdf differ