From 1d3d25e6ae39dfd0b52c2a30e8caf5ea8f97ae51 Mon Sep 17 00:00:00 2001 From: shinya Date: Thu, 28 May 2026 10:42:05 +0200 Subject: [PATCH] sloppified ai example but working, yaaaaaay, :,) --- chip_test_example/deploy.sh | 4 +- chip_test_example/lora_rx.cpp | 63 ++- chip_test_example/lora_tx.cpp | 18 +- chip_test_example/lr1121_malnus.hpp | 826 +++++++++++++--------------- test/flake.nix | 32 ++ 5 files changed, 472 insertions(+), 471 deletions(-) create mode 100644 test/flake.nix diff --git a/chip_test_example/deploy.sh b/chip_test_example/deploy.sh index 352a7d3..43adff8 100755 --- a/chip_test_example/deploy.sh +++ b/chip_test_example/deploy.sh @@ -39,10 +39,10 @@ deploy_and_build() { return $BUILD_STATUS } -deploy_and_build 10.91.51.165 & +deploy_and_build 10.91.51.166 & PID1=$! -deploy_and_build 10.91.51.166 & +deploy_and_build 10.91.51.165 & PID2=$! wait $PID1 diff --git a/chip_test_example/lora_rx.cpp b/chip_test_example/lora_rx.cpp index 90573c2..3cfcabd 100644 --- a/chip_test_example/lora_rx.cpp +++ b/chip_test_example/lora_rx.cpp @@ -1,5 +1,6 @@ -// lora_rx.cpp — LR1121 receive test -// Usage: sudo ./lora_rx [-v] [--433|--868|--24|freq_hz] [--reset] +// lora_rx.cpp — LR1121 receive test (robust RX loop) +// Usage: sudo ./lora_rx [-v] [--433|--868|--24|freq_hz] [--reset] [--tmo=ms] +// [--busy-gpio=N] [--reset-gpio=N] [--dio9-gpio=N] // -v verbose step labels (shows exactly where init hangs) // --433 433.05 MHz (default) // --868 868 MHz @@ -24,6 +25,7 @@ int main(int argc, char **argv) cfg.bw = 0x04; // 125 kHz cfg.cr = 0x01; // CR 4/5 bool do_reset = false; + uint32_t rx_timeout_ms = 1000; // Short timeout keeps RX loop responsive. for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "-v") == 0) cfg.verbose = true; @@ -32,11 +34,16 @@ int main(int argc, char **argv) else if (std::strcmp(argv[i], "--24") == 0 || std::strcmp(argv[i], "--2g4") == 0) cfg.freq_hz = lr1121::FREQ_2400; else if (std::strcmp(argv[i], "--reset") == 0) do_reset = true; + else if (std::strncmp(argv[i], "--tmo=", 6) == 0) rx_timeout_ms = (uint32_t)std::strtoul(argv[i] + 6, nullptr, 10); + else if (std::strncmp(argv[i], "--busy-gpio=", 12) == 0) cfg.busy_gpio = (unsigned)std::strtoul(argv[i] + 12, nullptr, 10); + else if (std::strncmp(argv[i], "--reset-gpio=", 13) == 0) cfg.reset_gpio = (unsigned)std::strtoul(argv[i] + 13, nullptr, 10); + else if (std::strncmp(argv[i], "--dio9-gpio=", 12) == 0) cfg.dio9_gpio = (unsigned)std::strtoul(argv[i] + 12, nullptr, 10); else cfg.freq_hz = (uint32_t)std::strtoul(argv[i], nullptr, 10); } - std::printf("lora_rx: %u Hz SF%u BW=0x%02X%s\n", + std::printf("lora_rx: %u Hz SF%u BW=0x%02X CR=0x%02X tmo=%ums gpio(busy=%u reset=%u dio9=%u)%s\n", cfg.freq_hz, cfg.sf, cfg.bw, + cfg.cr, rx_timeout_ms, cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio, cfg.verbose ? " [verbose]" : ""); if (do_reset) { @@ -75,18 +82,62 @@ int main(int argc, char **argv) uint8_t buf[256]; int pkt = 0; + int timeouts_in_row = 0; + int crc_in_row = 0; + uint32_t timeout_total = 0; + uint32_t crc_total = 0; + for (;;) { lr1121::RxInfo info{}; - int r = radio.receive(buf, (uint8_t)(sizeof(buf) - 1), 30'000, &info); + int r = radio.receive(buf, (uint8_t)(sizeof(buf) - 1), rx_timeout_ms, &info); if (r > 0) { buf[r] = '\0'; std::printf("[%4d] %d B rssi=%d dBm snr=%d dB '%s'\n", ++pkt, r, info.rssi_dbm, info.snr_db, buf); + timeouts_in_row = 0; + crc_in_row = 0; } else if (r == -1) { - std::printf(" timeout (30s), still listening...\n"); + ++timeouts_in_row; + ++timeout_total; + if ((timeout_total % 10u) == 0u) { + std::printf(" timeout x%u total (current streak=%d)\n", + timeout_total, timeouts_in_row); + } + + // If RX keeps stalling, fully reinitialize to recover reliably. + if (timeouts_in_row >= 20) { + std::printf(" RX stalled (timeouts streak=%d) -> reinitializing radio...\n", + timeouts_in_row); + radio.end(); + if (!radio.begin(cfg)) { + std::fprintf(stderr, "ERROR: radio reinit failed, retrying in 1s...\n"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + std::printf(" radio reinitialized, listening...\n"); + timeouts_in_row = 0; + crc_in_row = 0; + } + } } else if (r == -2) { - std::printf(" CRC error\n"); + ++crc_in_row; + ++crc_total; + std::printf(" CRC error (streak=%d total=%u)\n", crc_in_row, crc_total); + + if (crc_in_row >= 8) { + std::printf(" too many CRC errors -> reinitializing radio...\n"); + radio.end(); + if (!radio.begin(cfg)) { + std::fprintf(stderr, "ERROR: radio reinit failed, retrying in 1s...\n"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + std::printf(" radio reinitialized, listening...\n"); + timeouts_in_row = 0; + crc_in_row = 0; + } + } + } else { + std::printf(" RX unexpected code=%d\n", r); } } diff --git a/chip_test_example/lora_tx.cpp b/chip_test_example/lora_tx.cpp index 0ce7b5e..935ab17 100644 --- a/chip_test_example/lora_tx.cpp +++ b/chip_test_example/lora_tx.cpp @@ -1,5 +1,6 @@ // lora_tx.cpp — LR1121 transmit test -// Usage: sudo ./lora_tx [-v] [--433|--868|--24|freq_hz] +// Usage: sudo ./lora_tx [-v] [--433|--868|--24|freq_hz] [--lp|--hp] [--dbm=N] +// [--busy-gpio=N] [--reset-gpio=N] [--dio9-gpio=N] // -v verbose step labels (shows exactly where init hangs) // --433 433.05 MHz (default) // --868 868 MHz @@ -18,8 +19,8 @@ int main(int argc, char **argv) { lr1121::Config cfg; cfg.verbose = false; - cfg.pa_sel = 0x01; // HP PA — most modules; change to 0x00 for LP - cfg.tx_dbm = 14; + cfg.pa_sel = 0x00; // LP default: avoids LBD on weak VBAT rails + cfg.tx_dbm = 10; cfg.sf = 0x07; // SF7 cfg.bw = 0x04; // 125 kHz cfg.cr = 0x01; // CR 4/5 @@ -30,12 +31,19 @@ int main(int argc, char **argv) else if (std::strcmp(argv[i], "--868") == 0) cfg.freq_hz = lr1121::FREQ_868; else if (std::strcmp(argv[i], "--24") == 0 || std::strcmp(argv[i], "--2g4") == 0) cfg.freq_hz = lr1121::FREQ_2400; + else if (std::strcmp(argv[i], "--hp") == 0) cfg.pa_sel = 0x01; + else if (std::strcmp(argv[i], "--lp") == 0) cfg.pa_sel = 0x00; + else if (std::strncmp(argv[i], "--dbm=", 6) == 0) cfg.tx_dbm = (int8_t)std::atoi(argv[i] + 6); + else if (std::strncmp(argv[i], "--busy-gpio=", 12) == 0) cfg.busy_gpio = (unsigned)std::strtoul(argv[i] + 12, nullptr, 10); + else if (std::strncmp(argv[i], "--reset-gpio=", 13) == 0) cfg.reset_gpio = (unsigned)std::strtoul(argv[i] + 13, nullptr, 10); + else if (std::strncmp(argv[i], "--dio9-gpio=", 12) == 0) cfg.dio9_gpio = (unsigned)std::strtoul(argv[i] + 12, nullptr, 10); else cfg.freq_hz = (uint32_t)std::strtoul(argv[i], nullptr, 10); } - std::printf("lora_tx: %u Hz SF%u BW=0x%02X PA=%s%s\n", + std::printf("lora_tx: %u Hz SF%u BW=0x%02X PA=%s PWR=%d dBm gpio(busy=%u reset=%u dio9=%u)%s\n", cfg.freq_hz, cfg.sf, cfg.bw, - cfg.pa_sel ? "HP" : "LP", + cfg.pa_sel ? "HP" : "LP", (int)cfg.tx_dbm, + cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio, cfg.verbose ? " [verbose]" : ""); lr1121::Radio radio; diff --git a/chip_test_example/lr1121_malnus.hpp b/chip_test_example/lr1121_malnus.hpp index 2a42728..36fbd4c 100644 --- a/chip_test_example/lr1121_malnus.hpp +++ b/chip_test_example/lr1121_malnus.hpp @@ -1,18 +1,15 @@ -// lr1121_malnus.hpp — LR1121 LoRa driver for this exact board +// lr1121_malnus.hpp — minimal LR1121 LoRa driver for this 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) +// Board wiring: +// SPI : /dev/spidev0.0 (NSS GPIO8) +// BUSY : GPIO24 +// RESET: GPIO25 +// RF switch controlled by LR1121 DIO5/DIO6 // -// 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) +// Command set references: +// - Semtech LR1121 user manual rev 1.2 +// - Semtech SWDR001 +// - RadioLib LR11x0 behavior #pragma once #include @@ -28,80 +25,65 @@ namespace lr1121 { -// ── Opcodes ────────────────────────────────────────────────────────────────── -// System constexpr uint16_t OC_GET_VERSION = 0x0101; +constexpr uint16_t OC_WRITE_BUF8 = 0x0109; +constexpr uint16_t OC_READ_BUF8 = 0x010A; +constexpr uint16_t OC_CLEAR_RXBUF = 0x010B; 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 +constexpr uint16_t OC_CALIBRATE = 0x010F; +constexpr uint16_t OC_SET_REGMODE = 0x0110; +constexpr uint16_t OC_CALIBRATE_IMAGE = 0x0111; +constexpr uint16_t OC_SET_DIO_AS_RFSW = 0x0112; +constexpr uint16_t OC_SET_DIOIRQ = 0x0113; +constexpr uint16_t OC_CLEAR_IRQ = 0x0114; +constexpr uint16_t OC_REBOOT = 0x0118; +constexpr uint16_t OC_GET_VBAT = 0x0119; +constexpr uint16_t OC_SET_STANDBY = 0x011C; -// ── 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 +constexpr uint16_t OC_GET_RXBUF_STA = 0x0203; +constexpr uint16_t OC_GET_PKT_STATUS = 0x0204; +constexpr uint16_t OC_SET_LORA_NET = 0x0208; +constexpr uint16_t OC_SET_RX = 0x0209; +constexpr uint16_t OC_SET_TX = 0x020A; +constexpr uint16_t OC_SET_RF_FREQ = 0x020B; +constexpr uint16_t OC_SET_PKT_TYPE = 0x020E; +constexpr uint16_t OC_SET_MOD_PARAM = 0x020F; +constexpr uint16_t OC_SET_PKT_PARAM = 0x0210; +constexpr uint16_t OC_SET_TX_PARAMS = 0x0211; +constexpr uint16_t OC_SET_PKT_ADRS = 0x0212; +constexpr uint16_t OC_SET_FALLBACK_MODE = 0x0213; +constexpr uint16_t OC_SET_PA_CFG = 0x0215; -// Calibration bitmask (Calibrate command) -constexpr uint8_t CALIB_ALL = 0x3F; // LF_RC|HF_RC|PLL|ADC|IMG|PLL_TX +constexpr uint8_t CALIB_ALL = 0x3F; -// 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 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_LBD = (1u << 21); +constexpr uint32_t IRQ_ALL = 0x1BF80FFCu; 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 + unsigned busy_gpio = 24; + unsigned reset_gpio = 25; + unsigned dio9_gpio = 4; // IRQ line from LR1121 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 sf = 0x07; + uint8_t bw = 0x04; + uint8_t cr = 0x01; + int8_t tx_dbm = 10; + uint8_t pa_sel = 0x00; // LP default to avoid LBD on weak VBAT rails 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 lora_wan = false; bool verbose = false; }; @@ -113,535 +95,463 @@ struct RxInfo { struct ChipVersion { uint8_t hw; - uint8_t type; // 0x03 = LR1121 + uint8_t type; uint8_t fw_hi; uint8_t fw_lo; }; -// ── Radio class ─────────────────────────────────────────────────────────────── class Radio { public: bool begin(const Config &cfg); + bool beginRaw(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); + bool send(const uint8_t *data, uint8_t n); + 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; + int spi_fd_ = -1; + int busy_fd_ = -1; + int reset_fd_ = -1; + int dio9_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); + bool openSpi(const Config &cfg); + bool openGpio(unsigned line, bool out, int &fd_out); + int getGpioLine(int fd); + void setGpioLine(int fd, int value); + void hardReset(); + bool waitBusy(int timeout_ms = 1000); - // Verbose-mode helpers - static const char *modeName(uint8_t stat2); - static const char *cmdName(uint8_t stat1); + bool spiTransfer(uint8_t *buf, size_t len); + bool wcmd(uint16_t op, const uint8_t *params = nullptr, size_t n = 0); + bool rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr); - static void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2); - static uint8_t computeLDRO(uint8_t sf, uint8_t bw); + bool setStandbyXosc(); + bool setStandbyRc(); + bool setIrqMask(uint32_t irq1, uint32_t irq2 = 0); + bool clearIrq(uint32_t mask = IRQ_ALL); + uint32_t getIrq(); + uint16_t getErrors(); - // 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); + static void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2); + static uint8_t computeLdRo(uint8_t sf, uint8_t bw); + static uint32_t timeoutMsToRtcSteps(uint32_t ms); + 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) +inline bool Radio::spiTransfer(uint8_t *buf, size_t len) { 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.tx_buf = reinterpret_cast(buf); + tr.rx_buf = reinterpret_cast(buf); + tr.len = static_cast(len); + tr.speed_hz = cfg_.spi_hz; tr.bits_per_word = 8; - ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr); + return ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr) >= 0; } -// Write command: opcode [+ params]. Waits for BUSY before and after. -inline void Radio::wcmd(uint16_t op, const uint8_t *p, size_t n) +inline bool Radio::wcmd(uint16_t op, const uint8_t *params, 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(); + if (!waitBusy()) return false; + uint8_t cmd[258]{}; + cmd[0] = static_cast(op >> 8); + cmd[1] = static_cast(op & 0xFF); + if (params && n) std::memcpy(cmd + 2, params, n); + if (!spiTransfer(cmd, 2 + n)) return false; + return 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) +inline bool 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 false; + uint8_t cmd[32]{}; + cmd[0] = static_cast(op >> 8); + cmd[1] = static_cast(op & 0xFF); + if (params && np) std::memcpy(cmd + 2, params, np); + if (!spiTransfer(cmd, 2 + np)) return false; - if (!waitBusy()) return; - uint8_t rbuf[260]{}; - spiTransfer(rbuf, nr + 1); // rbuf[0] = stat1 (skip) - std::memcpy(out, rbuf + 1, nr); + if (!waitBusy()) return false; + uint8_t rsp[260]{}; + if (!spiTransfer(rsp, nr + 1)) return false; + std::memcpy(out, rsp + 1, nr); + return true; +} + +inline bool Radio::setIrqMask(uint32_t irq1, uint32_t irq2) +{ + const uint8_t p[8] = { + static_cast(irq1 >> 24), static_cast(irq1 >> 16), + static_cast(irq1 >> 8), static_cast(irq1), + static_cast(irq2 >> 24), static_cast(irq2 >> 16), + static_cast(irq2 >> 8), static_cast(irq2), + }; + return wcmd(OC_SET_DIOIRQ, p, sizeof(p)); +} + +inline bool Radio::clearIrq(uint32_t mask) +{ + const uint8_t p[4] = { + static_cast(mask >> 24), static_cast(mask >> 16), + static_cast(mask >> 8), static_cast(mask), + }; + return wcmd(OC_CLEAR_IRQ, p, sizeof(p)); } -// 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); + if (!spiTransfer(b, sizeof(b))) return 0; + return (static_cast(b[2]) << 24) | + (static_cast(b[3]) << 16) | + (static_cast(b[4]) << 8) | + (static_cast(b[5])); } 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]); + if (!rcmd(OC_GET_ERRORS, nullptr, 0, e, sizeof(e))) return 0xFFFF; + return static_cast((e[0] << 8) | e[1]); } -inline bool Radio::waitBusy() +inline bool Radio::setStandbyRc() { - 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; + const uint8_t p[] = {0x00}; + return wcmd(OC_SET_STANDBY, p, sizeof(p)); +} + +inline bool Radio::setStandbyXosc() +{ + const uint8_t p[] = {0x01}; + return wcmd(OC_SET_STANDBY, p, sizeof(p)); +} + +inline bool Radio::openSpi(const Config &cfg) +{ + spi_fd_ = ::open(cfg.spi_path, O_RDWR); + if (spi_fd_ < 0) return false; + + uint8_t mode = SPI_MODE_0; + uint8_t bits = 8; + if (ioctl(spi_fd_, SPI_IOC_WR_MODE, &mode) < 0) return false; + if (ioctl(spi_fd_, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) return false; + if (ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg.spi_hz) < 0) return false; + return true; } 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; - } + if (chip < 0) return false; + gpiohandle_request req{}; - req.lineoffsets[0] = line; - req.lines = 1; - req.flags = out ? GPIOHANDLE_REQUEST_OUTPUT : GPIOHANDLE_REQUEST_INPUT; + 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); + const int rc = 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; - } + if (rc < 0) 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); + if (ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &d) < 0) return 1; return d.values[0]; } +inline void Radio::setGpioLine(int fd, int value) +{ + gpiohandle_data d{}; + d.values[0] = value; + (void)ioctl(fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &d); +} + inline void Radio::hardReset() { setGpioLine(reset_fd_, 0); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); setGpioLine(reset_fd_, 1); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } -inline ChipVersion Radio::getVersion() +inline bool Radio::waitBusy(int timeout_ms) { - 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 "?"; + const int loops = timeout_ms * 20; + for (int i = 0; i < loops; ++i) { + if (getGpioLine(busy_fd_) == 0) return true; + std::this_thread::sleep_for(std::chrono::microseconds(50)); } + return false; } 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); + const uint32_t mhz = hz / 1'000'000u; + uint32_t lo = 430, hi = 440; + 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 = 902; hi = 928; + } else { + lo = 2400; hi = 2500; + } + f1 = static_cast(lo / 4); + f2 = static_cast((hi + 3) / 4); } -inline uint8_t Radio::computeLDRO(uint8_t sf, uint8_t bw) +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; + static const uint32_t bw_hz[] = {0, 15625, 31250, 62500, 125000, 250000, 500000}; + const uint32_t bw_val = (bw < 7) ? bw_hz[bw] : 125000; + const uint32_t sym_ms = (1000u << sf) / bw_val; + return (sym_ms > 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) +inline uint32_t Radio::timeoutMsToRtcSteps(uint32_t ms) { - if (pa_sel == 0x01) { // HP PA + if (ms == 0) return 0x00FFFFFFu; + const uint64_t steps = (static_cast(ms) * 32768u) / 1000u; + return (steps > 0x00FFFFFFu) ? 0x00FFFFFFu : static_cast(steps); +} + +inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max) +{ + if (pa_sel == 0x01) { 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 + } else { hp_max = 0; - if (dbm >= 14) { duty = 7; } - else if (dbm >= 10) { duty = 4; } - else if (dbm >= 0) { duty = 2; } - else { duty = 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) +inline ChipVersion Radio::getVersion() { - 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; + uint8_t v[4]{}; + (void)rcmd(OC_GET_VERSION, nullptr, 0, v, sizeof(v)); + return {v[0], v[1], v[2], v[3]}; } -inline bool Radio::begin(const Config &c) +inline bool Radio::beginRaw(const Config &cfg) { - 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)); + cfg_ = cfg; + if (!openSpi(cfg_)) return false; + if (!openGpio(cfg_.reset_gpio, true, reset_fd_)) return false; + if (!openGpio(cfg_.busy_gpio, false, busy_fd_)) return false; + if (!openGpio(cfg_.dio9_gpio, false, dio9_fd_)) return false; hardReset(); + return waitBusy(500); +} - 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); +inline bool Radio::begin(const Config &cfg) +{ + if (!beginRaw(cfg)) return false; - if (c.verbose) { + const ChipVersion v = getVersion(); + if (cfg_.verbose) { + std::fprintf(stderr, "[lr1121] hw=0x%02X type=0x%02X fw=0x%02X%02X\n", v.hw, v.type, v.fw_hi, v.fw_lo); 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" : ""); + if (rcmd(OC_GET_VBAT, nullptr, 0, &raw, 1)) { + const float vb = raw / 34.0f; + std::fprintf(stderr, "[lr1121] VBAT raw=0x%02X (~%.2fV)\n", raw, vb); + } } + if (v.type != 0x03) return false; -#define V(msg) do { if (c.verbose) std::fprintf(stderr, "[lr1121] -> " msg "\n"); } while(0) + if (!setStandbyRc()) return false; + { const uint8_t p[] = {0x01}; if (!wcmd(OC_SET_FALLBACK_MODE, p, sizeof(p))) return false; } + if (!clearIrq()) return false; + if (!setIrqMask(0, 0)) return false; + { const uint8_t p[] = {CALIB_ALL}; if (!wcmd(OC_CALIBRATE, p, sizeof(p))) return false; } + { const uint8_t p[] = {0x00}; (void)wcmd(OC_CLEAR_ERRORS, p, 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); + { const uint8_t p[] = {static_cast(cfg_.use_dcdc ? 0x01 : 0x00)}; if (!wcmd(OC_SET_REGMODE, p, sizeof(p))) return false; } + { const uint8_t p[] = {0x02}; if (!wcmd(OC_SET_PKT_TYPE, p, sizeof(p))) return false; } { - 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)" : ""); + const uint8_t p[] = { + static_cast(cfg_.freq_hz >> 24), static_cast(cfg_.freq_hz >> 16), + static_cast(cfg_.freq_hz >> 8), static_cast(cfg_.freq_hz), + }; + if (!wcmd(OC_SET_RF_FREQ, p, sizeof(p))) return false; } + { + uint8_t f1 = 0, f2 = 0; + imgCalFreqs(cfg_.freq_hz, f1, f2); + const uint8_t p[] = {f1, f2}; + if (!wcmd(OC_CALIBRATE_IMAGE, p, sizeof(p))) return false; + } + { const uint8_t p[] = {0x00, 0x00}; if (!wcmd(OC_SET_PKT_ADRS, p, sizeof(p))) return false; } + { + const uint8_t p[] = {cfg_.sf, cfg_.bw, cfg_.cr, computeLdRo(cfg_.sf, cfg_.bw)}; + if (!wcmd(OC_SET_MOD_PARAM, p, sizeof(p))) return false; + } + { const uint8_t p[] = {0x00, 0x08, 0x00, 0xFF, 0x01, 0x00}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; } - // ── Radio configuration ────────────────────────────────────────────────── - { const uint8_t a[] = {c.use_dcdc ? (uint8_t)0x01 : (uint8_t)0x00}; - wcmd(OC_SET_REGMODE, a, 1); } + { + uint8_t duty = 0, hp_max = 0; + computePaConfig(cfg_.pa_sel, cfg_.tx_dbm, duty, hp_max); + const uint8_t p[] = {cfg_.pa_sel, cfg_.pa_supply, duty, hp_max}; + if (!wcmd(OC_SET_PA_CFG, p, sizeof(p))) return false; + if (cfg_.verbose) { + std::fprintf(stderr, "[lr1121] PA: sel=0x%02X supply=0x%02X duty=%u hp_max=%u dbm=%d\n", + cfg_.pa_sel, cfg_.pa_supply, duty, hp_max, static_cast(cfg_.tx_dbm)); + } + } + { const uint8_t p[] = {static_cast(cfg_.tx_dbm), 0x02}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) return false; } + { const uint8_t p[] = {static_cast(cfg_.lora_wan ? 0x01 : 0x00)}; if (!wcmd(OC_SET_LORA_NET, p, sizeof(p))) return false; } - { const uint8_t a[] = {0x02}; wcmd(OC_SET_PKT_TYPE, a, 1); } // LoRa + // DIO5/DIO6 RF switch mapping: STBY(0,0), RX(1,0), TX(0,1), TX_HP(0,1) + { const uint8_t p[] = {0x03, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00, 0x00}; if (!wcmd(OC_SET_DIO_AS_RFSW, p, sizeof(p))) return false; } - { 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(); + if (!setStandbyXosc()) return false; + if (cfg_.verbose) std::fprintf(stderr, "[lr1121] init OK: %u Hz SF%u BW=0x%02X CR=0x%02X\n", cfg_.freq_hz, cfg_.sf, cfg_.bw, cfg_.cr); 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); + const uint8_t p[] = { static_cast(stay_in_bootloader ? 0x01 : 0x00) }; + (void)wcmd(OC_REBOOT, p, sizeof(p)); std::this_thread::sleep_for(std::chrono::milliseconds(300)); } inline void Radio::end() { - if (spi_fd_ >= 0) { ::close(spi_fd_); spi_fd_ = -1; } + if (spi_fd_ >= 0) { ::close(spi_fd_); spi_fd_ = -1; } + if (busy_fd_ >= 0) { ::close(busy_fd_); busy_fd_ = -1; } if (reset_fd_ >= 0) { ::close(reset_fd_); reset_fd_ = -1; } - if (busy_fd_ >= 0) { ::close(busy_fd_); busy_fd_ = -1; } + if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_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); } + if (n == 0) return false; + if (!setStandbyXosc()) return false; + if (!clearIrq()) return false; + if (!setIrqMask(IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD, IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD)) return false; - wcmd(OC_WRITE_BUF8, data, n); - { const uint8_t a[] = {0x00, 0x08, 0x00, n, 0x01, 0x00}; - wcmd(OC_SET_PKT_PARAM, a, 6); } + if (!wcmd(OC_WRITE_BUF8, data, n)) return false; + { const uint8_t p[] = {0x00, 0x08, 0x00, n, 0x01, 0x00}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; } - { 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()); + { + const uint32_t tx_steps = timeoutMsToRtcSteps(3000); + const uint8_t p[] = { + static_cast(tx_steps >> 16), + static_cast(tx_steps >> 8), + static_cast(tx_steps), + }; + if (!wcmd(OC_SET_TX, p, sizeof(p))) return false; } - for (int i = 0; i < 50'000; ++i) { - uint32_t irq = getIrq(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(3500); + while (std::chrono::steady_clock::now() < deadline) { + // Primary trigger is GPIO DIO9 (actual IRQ pin), then decode IRQ bits. + if (getGpioLine(dio9_fd_) == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + const uint32_t irq = getIrq(); if (irq & IRQ_TX_DONE) { - clearIrq(IRQ_ALL); - const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8); + (void)clearIrq(); + (void)setIrqMask(0, 0); return true; } - std::this_thread::sleep_for(std::chrono::microseconds(100)); + if (irq & (IRQ_TIMEOUT | IRQ_LBD)) { + if (cfg_.verbose) { + std::fprintf(stderr, "[lr1121] TX fail irq=0x%08X errs=0x%04X\n", irq, getErrors()); + if (irq & IRQ_LBD) std::fprintf(stderr, "[lr1121] LBD triggered: reduce TX power or use LP PA\n"); + } + (void)clearIrq(); + (void)setIrqMask(0, 0); + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - // 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); } + if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX poll timeout errs=0x%04X\n", getErrors()); + (void)clearIrq(); + (void)setIrqMask(0, 0); return false; } -inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, - RxInfo *rx_info) +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 + if (!setStandbyXosc()) return -1; + if (!clearIrq()) return -1; + if (!setIrqMask(IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT, IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT)) return -1; - { 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); } + const uint32_t steps = timeoutMsToRtcSteps(timeout_ms); + { + const uint8_t p[] = { + static_cast(steps >> 16), + static_cast(steps >> 8), + static_cast(steps), + }; + if (!wcmd(OC_SET_RX, p, sizeof(p))) return -1; + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms == 0 ? 60000 : timeout_ms + 500); 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 (std::chrono::steady_clock::now() >= deadline) { + (void)clearIrq(); + (void)setIrqMask(0, 0); + return -1; + } + if (getGpioLine(dio9_fd_) == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + const uint32_t irq = getIrq(); + if (irq & IRQ_TIMEOUT) { + (void)clearIrq(); + (void)setIrqMask(0, 0); + return -1; + } + if ((irq & (IRQ_RX_DONE | IRQ_CRC_ERR)) == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; } - if (irq & IRQ_TIMEOUT) { clearIrq(IRQ_ALL); return -1; } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (rx_info) { + uint8_t p[3]{}; + if (rcmd(OC_GET_PKT_STATUS, nullptr, 0, p, sizeof(p))) { + rx_info->rssi_dbm = static_cast(-(int)p[0] / 2); + rx_info->snr_db = static_cast(static_cast(p[1]) / 4); + rx_info->signal_rssi_dbm = static_cast(-(int)p[2] / 2); + } + } + + uint8_t st[2]{}; + if (!rcmd(OC_GET_RXBUF_STA, nullptr, 0, st, sizeof(st))) return -1; + const uint8_t len = (st[0] < cap) ? st[0] : cap; + const uint8_t p[] = {st[1], len}; + if (len > 0 && !rcmd(OC_READ_BUF8, p, sizeof(p), buf, len)) return -1; + + (void)wcmd(OC_CLEAR_RXBUF, nullptr, 0); + (void)clearIrq(); + (void)setIrqMask(0, 0); + return (irq & IRQ_CRC_ERR) ? -2 : static_cast(len); } } diff --git a/test/flake.nix b/test/flake.nix new file mode 100644 index 0000000..13d1146 --- /dev/null +++ b/test/flake.nix @@ -0,0 +1,32 @@ +{ + description = "cpp project"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + clang + clang-tools + pkg-config + + libcamera + libGL + mesa + + egl-wayland + ]; + + shellHook = '' + echo "dev shell ready" + ''; + }; + }; +}