// lr1121_malnus.hpp — LR1121 LoRa driver for this exact board // // Hardware: // /dev/spidev0.0 SCK=GPIO11, MOSI=GPIO10, MISO=GPIO9, NSS=GPIO8 // GPIO24 BUSY (DIO0) // GPIO25 nRESET // DIO5 RF-switch RFSW0 — chip-driven (HIGH in RX) // DIO6 RF-switch RFSW1 — chip-driven (HIGH in TX/TX_HP) // // Crystal oscillator module (no TCXO). SetDioAsRfSwitch configures DIO5/DIO6 // so the chip drives the module's RF switch automatically. // // Opcodes verified against: // LR1121 User Manual Rev 1.2 (UM.LR1121.W.APP, Table 13) // Semtech SWDR001 (https://github.com/Lora-net/SWDR001) #pragma once #include #include #include #include #include #include #include #include #include namespace lr1121 { // ── Opcodes ────────────────────────────────────────────────────────────────── // System constexpr uint16_t OC_GET_VERSION = 0x0101; constexpr uint16_t OC_GET_ERRORS = 0x010D; constexpr uint16_t OC_CLEAR_ERRORS = 0x010E; constexpr uint16_t OC_CALIBRATE = 0x010F; // 1B bitmask constexpr uint16_t OC_SET_REGMODE = 0x0110; // 1B: 0=LDO 1=DCDC constexpr uint16_t OC_CALIBRATE_IMAGE = 0x0111; // 2B: f1,f2 in 4-MHz steps constexpr uint16_t OC_SET_DIO_AS_RFSW = 0x0112; // 8B: enable + 7 mode bytes constexpr uint16_t OC_SET_DIOIRQ = 0x0113; // 8B: irq1(4B) + irq2(4B) constexpr uint16_t OC_CLEAR_IRQ = 0x0114; // 4B mask constexpr uint16_t OC_REBOOT = 0x0118; // 1B: 0=app 1=bootloader constexpr uint16_t OC_GET_VBAT = 0x0119; // → 1B raw ADC (VBAT ≈ raw/34.0 V) constexpr uint16_t OC_SET_STANDBY = 0x011C; // 1B: 0=RC 1=XOSC // Buffer constexpr uint16_t OC_WRITE_BUF8 = 0x0109; // N bytes constexpr uint16_t OC_READ_BUF8 = 0x010A; // 1B offset + 1B len → data constexpr uint16_t OC_CLEAR_RXBUF = 0x010B; // Radio constexpr uint16_t OC_GET_RXBUF_STA = 0x0203; // → [len, ptr] constexpr uint16_t OC_GET_PKT_STATUS = 0x0204; // → [rssi, snr, sig_rssi] constexpr uint16_t OC_SET_LORA_NET = 0x0208; // 1B: 0=private 1=public constexpr uint16_t OC_SET_RX = 0x0209; // 3B timeout in RTC steps constexpr uint16_t OC_SET_TX = 0x020A; // 3B timeout (0 = no timeout) constexpr uint16_t OC_SET_RF_FREQ = 0x020B; // 4B Hz big-endian constexpr uint16_t OC_SET_PKT_TYPE = 0x020E; // 1B: 0x02=LoRa constexpr uint16_t OC_SET_MOD_PARAM = 0x020F; // SF,BW,CR,LDRO constexpr uint16_t OC_SET_PKT_PARAM = 0x0210; // preamble(2B),hdr,len,crc,iq constexpr uint16_t OC_SET_TX_PARAMS = 0x0211; // pwr(int8),ramp constexpr uint16_t OC_SET_PKT_ADRS = 0x0212; // tx_base,rx_base constexpr uint16_t OC_SET_FALLBACK_MODE = 0x0213; // 1B: 0x01=STBY_RC constexpr uint16_t OC_SET_PA_CFG = 0x0215; // pa_sel,supply,duty,hp_max // ── Constants ───────────────────────────────────────────────────────────────── // IRQ bits (LR1121 UM Table GetStatus / SetDioIrqParams) constexpr uint32_t IRQ_TX_DONE = (1u << 2); constexpr uint32_t IRQ_RX_DONE = (1u << 3); constexpr uint32_t IRQ_CRC_ERR = (1u << 7); constexpr uint32_t IRQ_TIMEOUT = (1u << 10); constexpr uint32_t IRQ_ALL = 0x1BF80FFCu; // all defined IRQ bits // Calibration bitmask (Calibrate command) constexpr uint8_t CALIB_ALL = 0x3F; // LF_RC|HF_RC|PLL|ADC|IMG|PLL_TX // Modulation // SF: 0x05=SF5 .. 0x0C=SF12 // BW: 0x01=15.6kHz 0x02=31.2 0x03=62.5 0x04=125 0x05=250 0x06=500 // CR: 0x01=4/5 0x02=4/6 0x03=4/7 0x04=4/8 // PA: 0x00=LP (≤15 dBm) 0x01=HP (≤22 dBm) constexpr uint32_t FREQ_433 = 433'050'000u; constexpr uint32_t FREQ_868 = 868'000'000u; constexpr uint32_t FREQ_2400 = 2'403'000'000u; // ── Public types ────────────────────────────────────────────────────────────── struct Config { const char *spi_path = "/dev/spidev0.0"; uint32_t spi_hz = 8'000'000; const char *gpio_chip = "/dev/gpiochip0"; unsigned busy_gpio = 24; // DIO0 / BUSY unsigned reset_gpio = 25; // nRESET uint32_t freq_hz = FREQ_433; uint8_t sf = 0x07; // SF7 uint8_t bw = 0x04; // 125 kHz uint8_t cr = 0x01; // 4/5 int8_t tx_dbm = 14; // -17..+22 dBm uint8_t pa_sel = 0x01; // 0x00=LP(≤15dBm) 0x01=HP(≤22dBm) // pa_supply: 0x00=internal VREG (use for ≤14 dBm), 0x01=VBAT direct (>14 dBm) // RadioLib: "Must be set to PA_SUPPLY_VBAT when output power is more than 14 dBm." uint8_t pa_supply = 0x00; // use_dcdc: false=LDO (safe default for Pi; no DCDC inductor required) // true=DCDC (higher efficiency; only if module has external DCDC components) bool use_dcdc = false; bool lora_wan = false; // false = private sync word bool verbose = false; }; struct RxInfo { int8_t rssi_dbm = 0; int8_t snr_db = 0; int8_t signal_rssi_dbm = 0; }; struct ChipVersion { uint8_t hw; uint8_t type; // 0x03 = LR1121 uint8_t fw_hi; uint8_t fw_lo; }; // ── Radio class ─────────────────────────────────────────────────────────────── class Radio { public: bool begin(const Config &cfg); void end(); bool send(const uint8_t *data, uint8_t n); // true = TX_DONE, false = timeout // Returns byte count received; -1 = timeout, -2 = CRC error. int receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info = nullptr); ChipVersion getVersion(); // Open SPI + GPIOs and hard-reset only — no calibration. // Use for --reset / diagnostic commands when begin() would hang. bool beginRaw(const Config &cfg); void reboot(bool stay_in_bootloader = false); private: Config cfg_{}; int spi_fd_ = -1; int reset_fd_ = -1; int busy_fd_ = -1; void spiTransfer(uint8_t *buf, size_t n); void wcmd(uint16_t op, const uint8_t *p = nullptr, size_t n = 0); void rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr); uint32_t getIrq(); // NOP-poll: stat1,stat2,irq (no BUSY wait) void clearIrq(uint32_t mask); uint16_t getErrors(); // returns ErrorStat bits bool waitBusy(); void hardReset(); bool openGpio(unsigned line, bool out, int &fd_out); void setGpioLine(int fd, int val); int getGpioLine(int fd); bool openSpi(const Config &c); // Verbose-mode helpers static const char *modeName(uint8_t stat2); static const char *cmdName(uint8_t stat1); static void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2); static uint8_t computeLDRO(uint8_t sf, uint8_t bw); // Maps target output power to the recommended (PaDutyCycle, PaHpMax) per the // LR1121 datasheet PA Settings table. For HP PA: using max drive (duty=4, // hp_max=7) at low power draws 22 dBm worth of VBAT current even if SetTxParams // limits output — this droops VBAT and triggers LBD. For LP PA: PaHpMax is 0. static void computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max); }; // ── Implementation ──────────────────────────────────────────────────────────── inline void Radio::spiTransfer(uint8_t *buf, size_t n) { spi_ioc_transfer tr{}; tr.tx_buf = reinterpret_cast(buf); tr.rx_buf = reinterpret_cast(buf); tr.len = static_cast(n); tr.speed_hz = cfg_.spi_hz; tr.bits_per_word = 8; ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr); } // Write command: opcode [+ params]. Waits for BUSY before and after. inline void Radio::wcmd(uint16_t op, const uint8_t *p, size_t n) { if (!waitBusy()) return; uint8_t buf[260]; buf[0] = op >> 8; buf[1] = op & 0xFF; if (p && n) std::memcpy(buf + 2, p, n); spiTransfer(buf, 2 + n); waitBusy(); } // Read command: opcode [+ params], then clock NOP bytes to retrieve response. // Response byte 0 is stat1 (skipped); bytes 1..nr go to out[0..nr-1]. inline void Radio::rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr) { if (!waitBusy()) return; uint8_t cbuf[16]{}; cbuf[0] = op >> 8; cbuf[1] = op & 0xFF; if (params && np) std::memcpy(cbuf + 2, params, np); spiTransfer(cbuf, 2 + np); if (!waitBusy()) return; uint8_t rbuf[260]{}; spiTransfer(rbuf, nr + 1); // rbuf[0] = stat1 (skip) std::memcpy(out, rbuf + 1, nr); } // Poll current status via 6 NOP bytes (valid at any time, including during TX/RX). // Returns irq register [31:0]. MISO: [stat1, stat2, irq31:24, irq23:16, irq15:8, irq7:0]. inline uint32_t Radio::getIrq() { uint8_t b[6]{}; spiTransfer(b, 6); return ((uint32_t)b[2] << 24) | ((uint32_t)b[3] << 16) | ((uint32_t)b[4] << 8) | (uint32_t)b[5]; } inline void Radio::clearIrq(uint32_t mask) { uint8_t d[4] = { (uint8_t)(mask >> 24), (uint8_t)(mask >> 16), (uint8_t)(mask >> 8), (uint8_t)mask }; wcmd(OC_CLEAR_IRQ, d, 4); } inline uint16_t Radio::getErrors() { uint8_t e[2]{}; rcmd(OC_GET_ERRORS, nullptr, 0, e, 2); return (uint16_t)((e[0] << 8) | e[1]); } inline bool Radio::waitBusy() { for (int i = 0; i < 200'000; ++i) { if (!getGpioLine(busy_fd_)) return true; std::this_thread::sleep_for(std::chrono::microseconds(50)); } if (cfg_.verbose) std::fprintf(stderr, "[lr1121] waitBusy: TIMEOUT — chip hung?\n"); return false; } inline bool Radio::openGpio(unsigned line, bool out, int &fd_out) { int chip = ::open(cfg_.gpio_chip, O_RDWR); if (chip < 0) { if (cfg_.verbose) std::fprintf(stderr, "[lr1121] cannot open %s: %m\n", cfg_.gpio_chip); return false; } gpiohandle_request req{}; req.lineoffsets[0] = line; req.lines = 1; req.flags = out ? GPIOHANDLE_REQUEST_OUTPUT : GPIOHANDLE_REQUEST_INPUT; req.default_values[0] = out ? 1 : 0; std::strncpy(req.consumer_label, "lr1121", sizeof(req.consumer_label) - 1); int r = ioctl(chip, GPIO_GET_LINEHANDLE_IOCTL, &req); ::close(chip); if (r < 0) { if (cfg_.verbose) std::fprintf(stderr, "[lr1121] cannot get GPIO line %u: %m\n", line); return false; } fd_out = req.fd; return true; } inline void Radio::setGpioLine(int fd, int val) { gpiohandle_data d{}; d.values[0] = val; ioctl(fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &d); } inline int Radio::getGpioLine(int fd) { gpiohandle_data d{}; ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &d); return d.values[0]; } inline void Radio::hardReset() { setGpioLine(reset_fd_, 0); std::this_thread::sleep_for(std::chrono::milliseconds(10)); setGpioLine(reset_fd_, 1); std::this_thread::sleep_for(std::chrono::milliseconds(20)); } inline ChipVersion Radio::getVersion() { uint8_t v[4]{}; rcmd(OC_GET_VERSION, nullptr, 0, v, 4); return { v[0], v[1], v[2], v[3] }; } inline const char *Radio::modeName(uint8_t stat2) { switch ((stat2 >> 1) & 0x07) { case 0: return "SLEEP"; case 1: return "STBY_RC"; case 2: return "STBY_XOSC"; case 3: return "FS"; case 4: return "RX"; case 5: return "TX"; default: return "?"; } } inline const char *Radio::cmdName(uint8_t stat1) { switch ((stat1 >> 1) & 0x07) { case 0: return "CMD_FAIL"; case 1: return "CMD_PERR"; case 2: return "CMD_OK"; case 3: return "CMD_DAT"; default: return "?"; } } inline void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2) { // Frequency ranges per LR1121 UM Table CalibImage; steps = 4 MHz. uint32_t mhz = hz / 1'000'000u; uint32_t lo, hi; if (mhz < 446) { lo = 430; hi = 440; } else if (mhz < 740) { lo = 470; hi = 510; } else if (mhz < 890) { lo = 860; hi = 876; } else if (mhz < 2000) { lo = 900; hi = 928; } else { lo = 2400; hi = 2500; } f1 = (uint8_t)(lo / 4); f2 = (uint8_t)((hi + 3) / 4); } inline uint8_t Radio::computeLDRO(uint8_t sf, uint8_t bw) { // LDRO required when symbol duration > 16 ms. static const uint32_t bw_hz[] = { 0, 15625, 31250, 62500, 125000, 250000, 500000 }; uint32_t bw_val = (bw < 7) ? bw_hz[bw] : 125000; return ((1u << sf) * 1000u / bw_val > 16) ? 1 : 0; } // Maps target dBm to the LR1121 datasheet recommended (PaDutyCycle, PaHpMax). // // HP PA (pa_sel=0x01, sub-GHz) recommended table from LR1121 datasheet §PA config: // ≥22 dBm → duty=4, hp_max=7 ≥20 → 3,5 ≥17 → 2,3 // ≥14 dBm → duty=2, hp_max=2 ≥10 → 1,1 < 10 → 0,0 // // Using duty=4, hp_max=7 for lower power levels drives all 7 PA stages at full // current even though SetTxParams limits the output — VBAT droops under the // excess load and triggers LBD, aborting TX before the preamble starts. // // LP PA (pa_sel=0x00): PaHpMax is always 0; duty controls drive, hp_max unused. inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max) { if (pa_sel == 0x01) { // HP PA if (dbm >= 22) { duty = 4; hp_max = 7; } else if (dbm >= 20) { duty = 3; hp_max = 5; } else if (dbm >= 17) { duty = 2; hp_max = 3; } else if (dbm >= 14) { duty = 2; hp_max = 2; } else if (dbm >= 10) { duty = 1; hp_max = 1; } else { duty = 0; hp_max = 0; } } else { // LP PA — PaHpMax not applicable hp_max = 0; if (dbm >= 14) { duty = 7; } else if (dbm >= 10) { duty = 4; } else if (dbm >= 0) { duty = 2; } else { duty = 0; } } } inline bool Radio::openSpi(const Config &c) { spi_fd_ = ::open(c.spi_path, O_RDWR); if (spi_fd_ < 0) { if (c.verbose) std::fprintf(stderr, "[lr1121] cannot open %s: %m\n", c.spi_path); return false; } uint8_t mode = SPI_MODE_0, bits = 8; ioctl(spi_fd_, SPI_IOC_WR_MODE, &mode); ioctl(spi_fd_, SPI_IOC_WR_BITS_PER_WORD, &bits); ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &c.spi_hz); return true; } inline bool Radio::begin(const Config &c) { cfg_ = c; if (!openSpi(c)) return false; if (!openGpio(c.reset_gpio, true, reset_fd_)) return false; if (!openGpio(c.busy_gpio, false, busy_fd_)) return false; { uint8_t nop = 0x00; spiTransfer(&nop, 1); } std::this_thread::sleep_for(std::chrono::milliseconds(1)); hardReset(); ChipVersion ver = getVersion(); if (c.verbose) std::fprintf(stderr, "[lr1121] hw=0x%02X type=0x%02X fw=0x%02X%02X\n", ver.hw, ver.type, ver.fw_hi, ver.fw_lo); if (ver.type != 0x03) { if (c.verbose) std::fprintf(stderr, "[lr1121] unexpected type 0x%02X (want 0x03=LR1121)\n", ver.type); return false; } if (ver.fw_hi < 0x02 && c.verbose) std::fprintf(stderr, "[lr1121] warning: fw=0x%02X%02X — try: sudo ./lora_rx --reset\n", ver.fw_hi, ver.fw_lo); if (c.verbose) { uint8_t raw = 0; rcmd(OC_GET_VBAT, nullptr, 0, &raw, 1); // VBAT ≈ raw/34.0 V (from LR1121 formula: raw*5/(4*255)/1.091) float vbat = raw / 34.0f; std::fprintf(stderr, "[lr1121] VBAT raw=0x%02X (~%.2fV)%s\n", raw, vbat, (vbat < 2.5f && raw > 0) ? " ← LOW: check VBAT supply" : ""); } #define V(msg) do { if (c.verbose) std::fprintf(stderr, "[lr1121] -> " msg "\n"); } while(0) // ── Init sequence (RadioLib config() order, then per-band radio config) ── V("SetStandby(RC)"); { const uint8_t a[] = {0x00}; wcmd(OC_SET_STANDBY, a, 1); } // SetFallbackMode before Calibrate — matches RadioLib config() order. V("SetFallbackMode(STBY_RC)"); { const uint8_t a[] = {0x01}; wcmd(OC_SET_FALLBACK_MODE, a, 1); } V("ClearIrq / SetDioIrqParams(none)"); clearIrq(IRQ_ALL); { const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8); } V("Calibrate(ALL)"); { const uint8_t a[] = {CALIB_ALL}; wcmd(OC_CALIBRATE, a, 1); } // Calibrate can take up to several hundred ms. RadioLib does delay(5) + waitBusy. std::this_thread::sleep_for(std::chrono::milliseconds(5)); if (!waitBusy()) { if (c.verbose) std::fprintf(stderr, "[lr1121] Calibrate: BUSY stuck\n"); return false; } wcmd(OC_CLEAR_ERRORS); { uint16_t errs = getErrors(); if (errs && c.verbose) std::fprintf(stderr, "[lr1121] errors after calibrate: 0x%04X%s\n", errs, (errs & (1u<<5)) ? " (HF_XOSC_START_ERR — check crystal)" : ""); } // ── Radio configuration ────────────────────────────────────────────────── { const uint8_t a[] = {c.use_dcdc ? (uint8_t)0x01 : (uint8_t)0x00}; wcmd(OC_SET_REGMODE, a, 1); } { const uint8_t a[] = {0x02}; wcmd(OC_SET_PKT_TYPE, a, 1); } // LoRa { uint32_t f = c.freq_hz; const uint8_t a[] = { (uint8_t)(f>>24),(uint8_t)(f>>16),(uint8_t)(f>>8),(uint8_t)f }; wcmd(OC_SET_RF_FREQ, a, 4); } { uint8_t f1, f2; imgCalFreqs(c.freq_hz, f1, f2); const uint8_t a[] = {f1, f2}; wcmd(OC_CALIBRATE_IMAGE, a, 2); if (c.verbose) std::fprintf(stderr, "[lr1121] image cal: 0x%02X 0x%02X\n", f1, f2); } { const uint8_t a[] = {0x00, 0x00}; wcmd(OC_SET_PKT_ADRS, a, 2); } { uint8_t ldro = computeLDRO(c.sf, c.bw); const uint8_t a[] = {c.sf, c.bw, c.cr, ldro}; wcmd(OC_SET_MOD_PARAM, a, 4); if (c.verbose) std::fprintf(stderr, "[lr1121] SF=%u BW=0x%02X CR=0x%02X LDRO=%u\n", c.sf, c.bw, c.cr, ldro); } { const uint8_t a[] = {0x00, 0x08, 0x00, 0xFF, 0x01, 0x00}; wcmd(OC_SET_PKT_PARAM, a, 6); } // preamble=8, explicit hdr, maxlen=255, CRC=on // PA configuration: use the power-matched (duty, hp_max) per the LR1121 // datasheet. Oversized PA drive (duty=4, hp_max=7) at low power draws // excess VBAT current, droops the supply, and fires LBD aborting TX. { uint8_t duty, hp_max; computePaConfig(c.pa_sel, c.tx_dbm, duty, hp_max); const uint8_t a[] = {c.pa_sel, c.pa_supply, duty, hp_max}; wcmd(OC_SET_PA_CFG, a, 4); if (c.verbose) std::fprintf(stderr, "[lr1121] PA: sel=0x%02X supply=0x%02X duty=%u hp_max=%u\n", c.pa_sel, c.pa_supply, duty, hp_max); } { const uint8_t a[] = {(uint8_t)(int8_t)c.tx_dbm, 0x02}; wcmd(OC_SET_TX_PARAMS, a, 2); } { const uint8_t a[] = {c.lora_wan ? (uint8_t)0x01 : (uint8_t)0x00}; wcmd(OC_SET_LORA_NET, a, 1); } // DIO5 = RFSW0, DIO6 = RFSW1 (chip-driven RF switch). // standby: both LOW | rx: DIO5=HIGH | tx/tx_hp: DIO6=HIGH { const uint8_t a[] = {0x03, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00, 0x00}; wcmd(OC_SET_DIO_AS_RFSW, a, 8); if (c.verbose) std::fprintf(stderr, "[lr1121] RF switch: DIO5/DIO6\n"); } // Switch to crystal oscillator standby so the crystal is already running // when SetTx is issued — avoids cold-start failure on crystal-based modules. V("SetStandby(XOSC)"); { const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); } #undef V if (c.verbose) std::fprintf(stderr, "[lr1121] init OK: %u Hz SF%u PA=%s\n", c.freq_hz, c.sf, c.pa_sel ? "HP" : "LP"); return true; } inline bool Radio::beginRaw(const Config &c) { cfg_ = c; if (!openSpi(c)) return false; if (!openGpio(c.reset_gpio, true, reset_fd_)) return false; if (!openGpio(c.busy_gpio, false, busy_fd_)) return false; { uint8_t nop = 0x00; spiTransfer(&nop, 1); } std::this_thread::sleep_for(std::chrono::milliseconds(1)); hardReset(); return true; } inline void Radio::reboot(bool stay_in_bootloader) { const uint8_t a[] = { (uint8_t)(stay_in_bootloader ? 0x01 : 0x00) }; wcmd(OC_REBOOT, a, 1); std::this_thread::sleep_for(std::chrono::milliseconds(300)); } inline void Radio::end() { if (spi_fd_ >= 0) { ::close(spi_fd_); spi_fd_ = -1; } if (reset_fd_ >= 0) { ::close(reset_fd_); reset_fd_ = -1; } if (busy_fd_ >= 0) { ::close(busy_fd_); busy_fd_ = -1; } } inline bool Radio::send(const uint8_t *data, uint8_t n) { // Crystal must be warm for TX: switch to XOSC standby before SetTx. { const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); } wcmd(OC_WRITE_BUF8, data, n); { const uint8_t a[] = {0x00, 0x08, 0x00, n, 0x01, 0x00}; wcmd(OC_SET_PKT_PARAM, a, 6); } { const uint8_t a[] = {0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00}; wcmd(OC_SET_DIOIRQ, a, 8); } // DIO9: TX_DONE(bit2) | TIMEOUT(bit10) clearIrq(IRQ_ALL); { const uint8_t a[] = {0x00, 0x00, 0x00}; wcmd(OC_SET_TX, a, 3); } // wcmd() already waited for BUSY low (= PA ramped, preamble on air). if (cfg_.verbose) { uint8_t b[6]{}; spiTransfer(b, 6); uint32_t irq = ((uint32_t)b[2]<<24)|((uint32_t)b[3]<<16)|((uint32_t)b[4]<<8)|b[5]; std::fprintf(stderr, "[lr1121] after SetTx: mode=%s cmd=%s irq=0x%08X errs=0x%04X\n", modeName(b[1]), cmdName(b[0]), irq, getErrors()); } for (int i = 0; i < 50'000; ++i) { uint32_t irq = getIrq(); if (irq & IRQ_TX_DONE) { clearIrq(IRQ_ALL); const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8); return true; } std::this_thread::sleep_for(std::chrono::microseconds(100)); } // Timeout — print diagnostics then clean up. if (cfg_.verbose) { uint8_t b[6]{}; spiTransfer(b, 6); uint32_t irq = ((uint32_t)b[2]<<24)|((uint32_t)b[3]<<16)|((uint32_t)b[4]<<8)|b[5]; std::fprintf(stderr, "[lr1121] TX timeout: mode=%s cmd=%s irq=0x%08X errs=0x%04X\n", modeName(b[1]), cmdName(b[0]), irq, getErrors()); if (irq & (1u<<21)) std::fprintf(stderr, "[lr1121] ↳ LBD (Low Battery Detect): VBAT drooped under PA load.\n" "[lr1121] Check VBAT supply / decoupling. Try lower tx_dbm or pa_sel=LP.\n"); } clearIrq(IRQ_ALL); const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8); { const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); } return false; } inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info) { { const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); } // XOSC warm { const uint8_t a[] = {0x00, 0x00, 0x04, 0x88, 0x00, 0x00, 0x00, 0x00}; wcmd(OC_SET_DIOIRQ, a, 8); } // RX_DONE(bit3)|CRC_ERR(bit7)|TIMEOUT(bit10) clearIrq(IRQ_ALL); uint32_t t = (timeout_ms == 0) ? 0x00FF'FFFFu : (uint32_t)(timeout_ms * 32768ull / 1000); { const uint8_t a[] = {(uint8_t)(t>>16),(uint8_t)(t>>8),(uint8_t)t}; wcmd(OC_SET_RX, a, 3); } for (;;) { uint32_t irq = getIrq(); if (irq & IRQ_RX_DONE) { if (rx_info) { uint8_t ps[3]{}; rcmd(OC_GET_PKT_STATUS, nullptr, 0, ps, 3); rx_info->rssi_dbm = (int8_t)(-(int)ps[0] / 2); rx_info->snr_db = (int8_t)((int8_t)ps[1] / 4); rx_info->signal_rssi_dbm = (int8_t)(-(int)ps[2] / 2); } if (irq & IRQ_CRC_ERR) { clearIrq(IRQ_ALL); return -2; } uint8_t stat[2]{}; rcmd(OC_GET_RXBUF_STA, nullptr, 0, stat, 2); uint8_t len = (stat[0] < cap) ? stat[0] : cap; uint8_t p[2] = { stat[1], len }; rcmd(OC_READ_BUF8, p, 2, buf, len); wcmd(OC_CLEAR_RXBUF); clearIrq(IRQ_ALL); return (int)len; } if (irq & IRQ_TIMEOUT) { clearIrq(IRQ_ALL); return -1; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } // namespace lr1121