编写一个给M5CardputerADV用的红外码学习与发射程序,重点优化学习发射空调空外码,避免缓冲...

Criado em: 7 de julho de 2026

Respondido usando GPT-5.5 Thinking por Chat01

Pergunta

编写一个给M5CardputerADV用的红外码学习与发射程序,重点优化学习发射空调空外码,避免缓冲区小或空调本身用了非常规的长时序导致学习发射失败。避免插入SD卡之后,SD卡初始化把屏幕正在用的SPI总线引脚配置给覆盖掉了,导致插入SD卡后屏幕不刷新,无法学习。
可以在设置中修改红外接收管脚与发射管脚,要利用到cardputer的那个屏幕,也就是要有ui显示,一定要显示保存的红外码(十六进制或别的人类可读形式)。
学习出来的红外码可以让用户选择保存格式,一定要支持.ir文件,还可以支持raw时序脉冲文件,可以用irremote库解码成十六进制红外码显示出来。
所有配置文件都默认保存在SD卡一个文件夹中,不要散落根目录,如果没有SD卡,也可以让用户选择是否保存在内置存储。当然,如果用户没有修改任何默认配置,就不需要保存配置文件,免得污染储存空间。
M5CardputerADV的所有信息与管脚自己上网查,用M5CardputerADV官方打包的https://github.com/m5stack/M5Cardputer库,不要用esp32s3或arduion这种底层,尽量让代码简短精简。
.ir文件参考:
Filetype: IR signals file
Version: 1

Sony Bravia

name: Power
type: parsed
protocol: SIRC
address: 01 00 00 00
command: 15 00 00 00

name: Vol_dn
type: parsed
protocol: SIRC
address: 01 00 00 00
command: 3B 00 00 00

name: Ch_next
type: parsed
protocol: SIRC
address: 01 00 00 00
command: 10 00 00 00

name: Ch_prev
type: parsed
protocol: SIRC
address: 01 00 00 00
command: 11 00 00 00

Pensamento

undefined

Pensamento

undefined

Pensamento

undefined

Pensamento

Handling long IR signals and overflow concerns

When sending IR signals, I may need to handle very long timings that exceed uint16_t limits. For that, I'll use uint32_t to store raw values, preventing overflow. For transmission, I'll split long marks into manageable chunks, while space already supports uint32_t. This approach should handle edge cases.

Thought for 2m 37s

Resposta

下面这版按你的需求做了几件重点处理:Cardputer-Adv 官方 PinMap 中,内置 IR TX 是 G44,microSD 是 CS/MOSI/CLK/MISO = G12/G14/G40/G39,Grove 是 G2/G1;所以默认外接红外接收头用 G2,默认发射用内置 G44。(M5Stack Docs)
SD 初始化没有用全局 SPI,而是单独建了 SPIClass sdSPI(HSPI),避免 SPI.begin(...) 重新配置屏幕正在用的 SPI 状态;官方 SD 示例使用的是 SPI.begin(SD_SPI_SCK_PIN, SD_SPI_MISO_PIN, SD_SPI_MOSI_PIN, SD_SPI_CS_PIN)SD.begin(..., SPI, ...),这正是容易和屏幕刷新互相影响的地方。(GitHub)
红外部分用 IRremoteESP8266,该库支持 ESP32/ESP8266 的收发,并且官方示例也建议对空调码用更大的接收缓冲区;示例里明确说空调协议可能有 20–40 ms 甚至更长的分包间隔,kCaptureBufferSize = 1024kTimeout = 50 是为 AC 码设计的基础做法。这里我进一步提高到 4096 和默认 90 ms。(crankyoldgit.github.io)

cpp
/* M5Cardputer-ADV IR Learner / Sender - Built-in IR TX default: G44 - External IR receiver default: G2, e.g. Grove yellow pin - SD uses private SPIClass, not global SPI, to avoid breaking LCD refresh - Optimized for long AC IR frames: large raw buffer, long timeout, raw replay - Saves under /irlearn only; no files scattered in root - Supports .ir raw format and .raw pulse format - Config is saved only after user changes defaults Arduino Libraries: M5Cardputer by m5stack IRremoteESP8266 by crankyoldgit */ #include <Arduino.h> #include <M5Cardputer.h> #include <M5GFX.h> #include <SPI.h> #include <SD.h> #include <LittleFS.h> #include <IRremoteESP8266.h> #include <IRrecv.h> #include <IRsend.h> #include <IRutils.h> #include <IRac.h> #include <IRtext.h> #define APP_DIR "/irlearn" #define CFG_FILE "/irlearn/config.txt" #define SD_SCK_PIN 40 #define SD_MISO_PIN 39 #define SD_MOSI_PIN 14 #define SD_CS_PIN 12 #define DEFAULT_RX_PIN 2 #define DEFAULT_TX_PIN 44 #define MAX_RAW_PULSES 4096 #define CAPTURE_BUF 4096 SPIClass sdSPI(HSPI); M5Canvas canvas(&M5Cardputer.Display); struct Config { uint16_t rxPin = DEFAULT_RX_PIN; uint16_t txPin = DEFAULT_TX_PIN; uint8_t saveFmt = 0; // 0=.ir, 1=.raw uint8_t timeoutMs = 90; // AC friendly. Max normally <= 130 ms. uint8_t tolerance = 30; // default library tolerance is usually 25 bool useLittleFS = false; // default: SD } cfg; struct LearnedIR { bool valid = false; String name = ""; String protocol = ""; String hex = ""; String acDesc = ""; uint16_t bits = 0; uint32_t address = 0; uint32_t command = 0; uint32_t freq = 38000; bool overflow = false; uint16_t rawLen = 0; uint32_t raw[MAX_RAW_PULSES]; // use uint32_t to keep very long spaces safe } last; IRrecv *irrecv = nullptr; IRsend *irsend = nullptr; decode_results results; bool sdOK = false; bool littleOK = false; String statusLine = "Ready"; String files[48]; uint8_t fileCount = 0; int fileIndex = -1; bool defaultsUnchanged() { return cfg.rxPin == DEFAULT_RX_PIN && cfg.txPin == DEFAULT_TX_PIN && cfg.saveFmt == 0 && cfg.timeoutMs == 90 && cfg.tolerance == 30 && cfg.useLittleFS == false; } fs::FS *activeFS() { if (cfg.useLittleFS) return littleOK ? &LittleFS : nullptr; if (sdOK) return &SD; return nullptr; } const char *storeName() { if (cfg.useLittleFS) return "LittleFS"; return sdOK ? "SD" : "SD missing"; } void ensureDir(fs::FS &fs) { if (!fs.exists(APP_DIR)) fs.mkdir(APP_DIR); } String lowerKey(char c) { if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; return String(c); } String fmtHex32(uint32_t v) { char buf[11]; snprintf(buf, sizeof(buf), "0x%08lX", (unsigned long)v); return String(buf); } void drawUI() { canvas.fillScreen(BLACK); canvas.setCursor(0, 0); canvas.setTextColor(GREEN); canvas.setTextSize(1); canvas.printf("M5Cardputer ADV IR\n"); canvas.printf("RX:G%d TX:G%d Store:%s\n", cfg.rxPin, cfg.txPin, storeName()); canvas.printf("Fmt:%s T/O:%dms Tol:%d%%\n", cfg.saveFmt == 0 ? ".ir" : ".raw", cfg.timeoutMs, cfg.tolerance); canvas.println("------------------------------"); if (last.valid) { canvas.printf("Name:%s\n", last.name.c_str()); canvas.printf("Proto:%s Bits:%u\n", last.protocol.c_str(), last.bits); canvas.printf("HEX:%s\n", last.hex.substring(0, 28).c_str()); if (last.hex.length() > 28) canvas.printf(" %s\n", last.hex.substring(28, 56).c_str()); canvas.printf("Addr:%s Cmd:%s\n", fmtHex32(last.address).c_str(), fmtHex32(last.command).c_str()); canvas.printf("Raw:%u pulses%s\n", last.rawLen, last.overflow ? " OVERFLOW!" : ""); if (last.rawLen) { canvas.print("Data:"); for (uint8_t i = 0; i < min((uint16_t)8, last.rawLen); i++) { canvas.printf(" %lu", (unsigned long)last.raw[i]); } canvas.println(last.rawLen > 8 ? " ..." : ""); } if (last.acDesc.length()) { canvas.printf("AC:%s\n", last.acDesc.substring(0, 45).c_str()); } } else { canvas.println("No IR learned/loaded."); } canvas.println("------------------------------"); canvas.println("L learn P send S save N file"); canvas.println("F fmt R rxpin T txpin O store"); canvas.println("A timeout D tolerance"); if (fileIndex >= 0 && fileIndex < fileCount) { canvas.printf("Sel:%s\n", files[fileIndex].c_str()); } canvas.printf("Status:%s\n", statusLine.c_str()); canvas.pushSprite(0, 0); } char getKey() { M5Cardputer.update(); if (!M5Cardputer.Keyboard.isChange()) return 0; if (!M5Cardputer.Keyboard.isPressed()) return 0; Keyboard_Class::KeysState st = M5Cardputer.Keyboard.keysState(); if (st.enter) return '\n'; if (st.del) return '\b'; for (auto c : st.word) return c; return 0; } int promptNumber(const String &title, int current, int minV, int maxV) { String buf = String(current); while (true) { canvas.fillScreen(BLACK); canvas.setCursor(0, 0); canvas.setTextColor(GREEN); canvas.setTextSize(1); canvas.println(title); canvas.println("Type number, Enter=OK, Del=back"); canvas.println(); canvas.printf("> %s\n", buf.c_str()); canvas.pushSprite(0, 0); char c = getKey(); if (!c) { delay(10); continue; } if (c >= '0' && c <= '9') { if (buf.length() < 5) buf += c; } else if (c == '\b') { if (buf.length()) buf.remove(buf.length() - 1); } else if (c == '\n') { int v = buf.toInt(); if (v < minV) v = minV; if (v > maxV) v = maxV; return v; } } } void writeConfigIfNeeded() { fs::FS *fs = activeFS(); if (!fs) { statusLine = "No storage for config"; return; } ensureDir(*fs); if (defaultsUnchanged()) { if (fs->exists(CFG_FILE)) fs->remove(CFG_FILE); statusLine = "Defaults: no config file"; return; } File f = fs->open(CFG_FILE, FILE_WRITE); if (!f) { statusLine = "Config write failed"; return; } f.printf("rxPin=%u\n", cfg.rxPin); f.printf("txPin=%u\n", cfg.txPin); f.printf("saveFmt=%u\n", cfg.saveFmt); f.printf("timeoutMs=%u\n", cfg.timeoutMs); f.printf("tolerance=%u\n", cfg.tolerance); f.printf("useLittleFS=%u\n", cfg.useLittleFS ? 1 : 0); f.close(); statusLine = "Config saved"; } void parseConfigFile(fs::FS &fs) { if (!fs.exists(CFG_FILE)) return; File f = fs.open(CFG_FILE, FILE_READ); if (!f) return; while (f.available()) { String line = f.readStringUntil('\n'); line.trim(); int eq = line.indexOf('='); if (eq < 0) continue; String k = line.substring(0, eq); String v = line.substring(eq + 1); if (k == "rxPin") cfg.rxPin = v.toInt(); else if (k == "txPin") cfg.txPin = v.toInt(); else if (k == "saveFmt") cfg.saveFmt = v.toInt() ? 1 : 0; else if (k == "timeoutMs") cfg.timeoutMs = constrain(v.toInt(), 15, 130); else if (k == "tolerance") cfg.tolerance = constrain(v.toInt(), 10, 50); else if (k == "useLittleFS") cfg.useLittleFS = v.toInt() != 0; } f.close(); } void rebuildIR() { if (irrecv) { irrecv->disableIRIn(); delete irrecv; } if (irsend) delete irsend; irrecv = new IRrecv(cfg.rxPin, CAPTURE_BUF, cfg.timeoutMs, true); irrecv->setTolerance(cfg.tolerance); irrecv->setUnknownThreshold(24); irrecv->enableIRIn(); irsend = new IRsend(cfg.txPin); irsend->begin(); irsend->calibrate(38000); } void mountStorage() { littleOK = LittleFS.begin(true); // Important: private SPI bus, never global SPI. sdSPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN); sdOK = SD.begin(SD_CS_PIN, sdSPI, 20000000); if (sdOK) { ensureDir(SD); parseConfigFile(SD); } else if (littleOK) { parseConfigFile(LittleFS); } } void refreshFiles() { fileCount = 0; fileIndex = -1; fs::FS *fs = activeFS(); if (!fs) return; ensureDir(*fs); File root = fs->open(APP_DIR); if (!root || !root.isDirectory()) return; File f = root.openNextFile(); while (f && fileCount < 48) { String n = String(f.name()); if (n.endsWith(".ir") || n.endsWith(".raw")) { int slash = n.lastIndexOf('/'); if (slash >= 0) n = n.substring(slash + 1); files[fileCount++] = n; } f = root.openNextFile(); } if (fileCount > 0) fileIndex = 0; } void copyRawFromDecode(decode_results &r) { last.rawLen = 0; if (r.rawlen <= 1) return; for (uint16_t i = 1; i < r.rawlen && last.rawLen < MAX_RAW_PULSES; i++) { last.raw[last.rawLen++] = (uint32_t)r.rawbuf[i] * kRawTick; } } void learnIR() { statusLine = "Learning... aim remote"; drawUI(); last.valid = false; uint32_t started = millis(); while (millis() - started < 25000) { M5Cardputer.update(); if (irrecv->decode(&results)) { last.valid = true; last.name = "code_" + String(millis()); last.protocol = typeToString(results.decode_type, results.repeat); last.hex = resultToHexidecimal(&results); last.bits = results.bits; last.address = results.address; last.command = results.command; last.overflow = results.overflow; last.freq = 38000; last.acDesc = IRAcUtils::resultAcToString(&results); copyRawFromDecode(results); irrecv->resume(); if (last.overflow || last.rawLen >= MAX_RAW_PULSES) { statusLine = "Learned, but buffer full"; } else { statusLine = "Learned OK"; } drawUI(); return; } delay(5); } statusLine = "Learn timeout"; drawUI(); } void sendRawLong(const LearnedIR &ir) { if (!ir.valid || ir.rawLen == 0 || !irsend) { statusLine = "Nothing to send"; return; } irrecv->disableIRIn(); irsend->enableIROut(ir.freq, 33); for (uint16_t i = 0; i < ir.rawLen; i++) { uint32_t usec = ir.raw[i]; if ((i & 1) == 0) { // mark() takes uint16_t, so split abnormal long marks safely. while (usec > 0) { uint16_t chunk = usec > 60000UL ? 60000 : usec; irsend->mark(chunk); usec -= chunk; } } else { irsend->space(usec); } if ((i % 128) == 0) yield(); } irsend->space(0); irrecv->enableIRIn(); statusLine = "Sent raw"; } void saveAsIR(fs::FS &fs, const String &path) { File f = fs.open(path, FILE_WRITE); if (!f) { statusLine = ".ir write failed"; return; } f.println("Filetype: IR signals file"); f.println("Version: 1"); f.println("#"); f.println("# M5Cardputer-ADV learned IR"); f.printf("# protocol: %s\n", last.protocol.c_str()); f.printf("# hex: %s\n", last.hex.c_str()); if (last.acDesc.length()) f.printf("# ac: %s\n", last.acDesc.c_str()); f.println("#"); f.printf("name: %s\n", last.name.c_str()); // Deliberately save .ir as raw for maximum AC compatibility. // Parsed address/command is displayed, but raw replay is more reliable for AC state frames. f.println("type: raw"); f.printf("frequency: %lu\n", (unsigned long)last.freq); f.println("duty_cycle: 0.330000"); f.print("data:"); for (uint16_t i = 0; i < last.rawLen; i++) { f.print(' '); f.print(last.raw[i]); if ((i % 128) == 127) yield(); } f.println(); f.close(); statusLine = "Saved .ir"; } void saveAsRAW(fs::FS &fs, const String &path) { File f = fs.open(path, FILE_WRITE); if (!f) { statusLine = ".raw write failed"; return; } f.println("# M5Cardputer-ADV raw IR pulses"); f.printf("name: %s\n", last.name.c_str()); f.printf("protocol: %s\n", last.protocol.c_str()); f.printf("hex: %s\n", last.hex.c_str()); f.printf("frequency: %lu\n", (unsigned long)last.freq); f.printf("rawlen: %u\n", last.rawLen); f.print("data:"); for (uint16_t i = 0; i < last.rawLen; i++) { f.print(i ? "," : " "); f.print(last.raw[i]); if ((i % 128) == 127) yield(); } f.println(); f.close(); statusLine = "Saved .raw"; } void saveLearned() { if (!last.valid || last.rawLen == 0) { statusLine = "Nothing learned"; drawUI(); return; } fs::FS *fs = activeFS(); if (!fs) { statusLine = "No storage. Press O."; drawUI(); return; } ensureDir(*fs); String ext = cfg.saveFmt == 0 ? ".ir" : ".raw"; String path = String(APP_DIR) + "/" + last.name + ext; if (cfg.saveFmt == 0) saveAsIR(*fs, path); else saveAsRAW(*fs, path); refreshFiles(); drawUI(); } void addRawNumber(uint32_t value) { if (last.rawLen < MAX_RAW_PULSES) { last.raw[last.rawLen++] = value; } } void parseNumberList(const String &s) { uint32_t v = 0; bool inNum = false; for (uint32_t i = 0; i < s.length(); i++) { char c = s[i]; if (c >= '0' && c <= '9') { inNum = true; v = v * 10 + (c - '0'); } else { if (inNum) { addRawNumber(v); v = 0; inNum = false; } } } if (inNum) addRawNumber(v); } bool loadSelectedFile() { if (fileIndex < 0 || fileIndex >= fileCount) { statusLine = "No selected file"; return false; } fs::FS *fs = activeFS(); if (!fs) { statusLine = "No storage"; return false; } String path = String(APP_DIR) + "/" + files[fileIndex]; File f = fs->open(path, FILE_READ); if (!f) { statusLine = "Open failed"; return false; } last = LearnedIR(); last.valid = true; last.name = files[fileIndex]; last.freq = 38000; last.protocol = "RAW_FILE"; last.hex = "(raw pulses)"; while (f.available()) { String line = f.readStringUntil('\n'); line.trim(); if (line.startsWith("frequency:")) { last.freq = line.substring(10).toInt(); if (last.freq < 1000) last.freq *= 1000; } else if (line.startsWith("# protocol:")) { last.protocol = line.substring(11); last.protocol.trim(); } else if (line.startsWith("# hex:")) { last.hex = line.substring(6); last.hex.trim(); } else if (line.startsWith("protocol:")) { last.protocol = line.substring(9); last.protocol.trim(); } else if (line.startsWith("hex:")) { last.hex = line.substring(4); last.hex.trim(); } else if (line.startsWith("data:")) { parseNumberList(line.substring(5)); } } f.close(); if (last.rawLen == 0) { last.valid = false; statusLine = "No raw data in file"; return false; } statusLine = "Loaded file"; return true; } void nextFile() { refreshFiles(); if (fileCount == 0) { statusLine = "No saved files"; return; } fileIndex++; if (fileIndex >= fileCount) fileIndex = 0; statusLine = "Selected " + files[fileIndex]; } void toggleStorage() { if (!littleOK) { statusLine = "LittleFS unavailable"; return; } cfg.useLittleFS = !cfg.useLittleFS; fs::FS *fs = activeFS(); if (fs) ensureDir(*fs); writeConfigIfNeeded(); refreshFiles(); } void setup() { Serial.begin(115200); auto m5cfg = M5.config(); M5Cardputer.begin(m5cfg, true); M5Cardputer.Display.setRotation(1); canvas.setColorDepth(8); canvas.createSprite(M5Cardputer.Display.width(), M5Cardputer.Display.height()); canvas.setTextScroll(false); mountStorage(); rebuildIR(); refreshFiles(); statusLine = sdOK ? "SD mounted" : "SD missing"; drawUI(); } void loop() { char c = getKey(); if (!c) { delay(10); return; } String k = lowerKey(c); if (k == "l") { learnIR(); } else if (k == "p") { if (fileIndex >= 0 && fileIndex < fileCount) loadSelectedFile(); sendRawLong(last); drawUI(); } else if (k == "s") { saveLearned(); } else if (k == "n") { nextFile(); drawUI(); } else if (k == "f") { cfg.saveFmt = cfg.saveFmt ? 0 : 1; writeConfigIfNeeded(); drawUI(); } else if (k == "o") { toggleStorage(); drawUI(); } else if (k == "r") { cfg.rxPin = promptNumber("Set IR RX pin", cfg.rxPin, 0, 48); rebuildIR(); writeConfigIfNeeded(); drawUI(); } else if (k == "t") { cfg.txPin = promptNumber("Set IR TX pin", cfg.txPin, 0, 48); rebuildIR(); writeConfigIfNeeded(); drawUI(); } else if (k == "a") { cfg.timeoutMs = promptNumber("Set timeout ms", cfg.timeoutMs, 15, 130); rebuildIR(); writeConfigIfNeeded(); drawUI(); } else if (k == "d") { cfg.tolerance = promptNumber("Set tolerance %", cfg.tolerance, 10, 50); rebuildIR(); writeConfigIfNeeded(); drawUI(); } }

几个使用建议:

  1. 外接红外接收头建议接 Grove 的 G2,供电按你的接收头模块要求接 3.3V 或 5V;Cardputer-Adv 自带的是发射管,不是接收管。
  2. 对空调码,建议保存为默认 .ir,但这份程序把 .ir 写成 raw 类型,而不是强行写 type: parsed。原因是空调遥控通常发送“整机状态帧”,长度常见 64–280+ bit,甚至有分包、校验和和较长间隔;只存 address/command 很容易复现失败。(DeepWiki)
  3. 屏幕不刷新问题的核心规避点是:不要在屏幕初始化后再用全局 SPI.begin(...) 改 SPI 引脚;这版只对 SD 使用 sdSPI.begin(...)
  4. 如果你的 Arduino-ESP32 包对 HSPI 报错,把这一行:
    cpp
    SPIClass sdSPI(HSPI);
    改成:
    cpp
    SPIClass sdSPI(FSPI);
    但优先保留独立 SPIClass,不要回到全局 SPI

Compartilhar esta Q&A