// lr1121_malnus.hpp — minimal LR1121 LoRa driver for this board // // Board wiring: // SPI : /dev/spidev0.0 (NSS GPIO8) // BUSY : GPIO24 // RESET: GPIO25 // RF switch controlled by LR1121 DIO5/DIO6 // // Command set references: // - Semtech LR1121 user manual rev 1.2 // - Semtech SWDR001 // - RadioLib LR11x0 behavior #pragma once #include #include #include #include #include #include #include #include #include #include namespace lr1121 { 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; 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_GET_IRQ = 0x0115; constexpr uint16_t OC_REBOOT = 0x0118; constexpr uint16_t OC_GET_VBAT = 0x0119; constexpr uint16_t OC_SET_STANDBY = 0x011C; 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; constexpr uint8_t CALIB_ALL = 0x3F; 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; constexpr uint8_t LORA_PKT_HEADER_EXPLICIT = 0x00; constexpr uint8_t LORA_PKT_PREAMBLE_LEN = 0x08; constexpr uint8_t LORA_PKT_CRC_ON = 0x01; constexpr uint8_t LORA_PKT_IQ_STD = 0x00; constexpr uint8_t TX_RAMP_48US = 0x02; constexpr uint8_t PKT_TYPE_LORA = 0x02; constexpr uint32_t TX_TIMEOUT_MS = 3000; constexpr uint32_t TX_POLL_GUARD_MS = 500; constexpr int8_t MIN_TX_DBM_FALLBACK = 2; 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; unsigned reset_gpio = 25; unsigned dio9_gpio = 4; // IRQ line from LR1121 uint32_t freq_hz = FREQ_433; 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; bool use_dcdc = false; bool lora_wan = false; 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; uint8_t fw_hi; uint8_t fw_lo; }; class Radio { public: bool begin(const Config &cfg); bool beginRaw(const Config &cfg); bool softResetSettings(); void end(); 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(); void reboot(bool stay_in_bootloader = false); private: Config cfg_{}; int spi_fd_ = -1; int busy_fd_ = -1; int reset_fd_ = -1; int dio9_fd_ = -1; 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); 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); 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(); bool applyRadioSettings(); 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); }; 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(len); tr.speed_hz = cfg_.spi_hz; tr.bits_per_word = 8; return ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr) >= 0; } inline bool Radio::wcmd(uint16_t op, const uint8_t *params, size_t n) { if (n > 256) return false; 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(); } inline bool Radio::rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr) { if (np > 30 || nr > 259) return false; 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 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)); } inline uint32_t Radio::getIrq() { uint8_t b[4]{}; if (!rcmd(OC_GET_IRQ, nullptr, 0, b, sizeof(b))) return 0; return (static_cast(b[0]) << 24) | (static_cast(b[1]) << 16) | (static_cast(b[2]) << 8) | static_cast(b[3]); } inline uint16_t Radio::getErrors() { uint8_t e[2]{}; if (!rcmd(OC_GET_ERRORS, nullptr, 0, e, sizeof(e))) return 0xFFFF; return static_cast((e[0] << 8) | e[1]); } inline bool Radio::setStandbyRc() { 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 || ioctl(spi_fd_, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 || ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg.spi_hz) < 0) { ::close(spi_fd_); spi_fd_ = -1; 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) 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); const int rc = ioctl(chip, GPIO_GET_LINEHANDLE_IOCTL, &req); ::close(chip); if (rc < 0) return false; fd_out = req.fd; return true; } inline int Radio::getGpioLine(int fd) { gpiohandle_data 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(1)); setGpioLine(reset_fd_, 1); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } inline bool Radio::waitBusy(int timeout_ms) { 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) { 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) { 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; } inline uint32_t Radio::timeoutMsToRtcSteps(uint32_t ms) { 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 { hp_max = 0; if (dbm >= 14) duty = 7; else if (dbm >= 10) duty = 4; else if (dbm >= 0) duty = 2; else duty = 0; } } inline ChipVersion Radio::getVersion() { 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::beginRaw(const Config &cfg) { end(); cfg_ = cfg; if (!openSpi(cfg_)) return false; if (!openGpio(cfg_.reset_gpio, true, reset_fd_)) { end(); return false; } if (!openGpio(cfg_.busy_gpio, false, busy_fd_)) { end(); return false; } if (!openGpio(cfg_.dio9_gpio, false, dio9_fd_)) { end(); return false; } hardReset(); if (!waitBusy(500)) { end(); return false; } return true; } inline bool Radio::begin(const Config &cfg) { if (!beginRaw(cfg)) return false; 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; 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) { end(); return false; } return applyRadioSettings(); } inline bool Radio::applyRadioSettings() { if (!setStandbyRc()) { end(); return false; } { const uint8_t p[] = {0x01}; if (!wcmd(OC_SET_FALLBACK_MODE, p, sizeof(p))) { end(); return false; } } if (!clearIrq()) { end(); return false; } if (!setIrqMask(0, 0)) { end(); return false; } { const uint8_t p[] = {CALIB_ALL}; if (!wcmd(OC_CALIBRATE, p, sizeof(p))) { end(); return false; } } (void)wcmd(OC_CLEAR_ERRORS, nullptr, 0); { const uint8_t p[] = {static_cast(cfg_.use_dcdc ? 0x01 : 0x00)}; if (!wcmd(OC_SET_REGMODE, p, sizeof(p))) { end(); return false; } } { const uint8_t p[] = {PKT_TYPE_LORA}; if (!wcmd(OC_SET_PKT_TYPE, p, sizeof(p))) { end(); return false; } } { 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))) { end(); 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))) { end(); return false; } } { const uint8_t p[] = {0x00, 0x00}; if (!wcmd(OC_SET_PKT_ADRS, p, sizeof(p))) { end(); 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))) { end(); return false; } } { const uint8_t p[] = {LORA_PKT_HEADER_EXPLICIT, LORA_PKT_PREAMBLE_LEN, 0x00, 0xFF, LORA_PKT_CRC_ON, LORA_PKT_IQ_STD}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) { end(); return false; } } { 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))) { end(); 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), TX_RAMP_48US}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) { end(); return false; } } { const uint8_t p[] = {static_cast(cfg_.lora_wan ? 0x01 : 0x00)}; if (!wcmd(OC_SET_LORA_NET, p, sizeof(p))) { end(); return false; } } // 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))) { end(); return false; } } if (!setStandbyXosc()) { end(); 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 bool Radio::softResetSettings() { if (spi_fd_ < 0 || busy_fd_ < 0 || reset_fd_ < 0 || dio9_fd_ < 0) return false; if (!setStandbyRc()) return false; (void)wcmd(OC_CLEAR_RXBUF, nullptr, 0); (void)clearIrq(); return applyRadioSettings(); } inline void Radio::reboot(bool stay_in_bootloader) { 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 (busy_fd_ >= 0) { ::close(busy_fd_); busy_fd_ = -1; } if (reset_fd_ >= 0) { ::close(reset_fd_); reset_fd_ = -1; } if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_fd_ = -1; } } inline bool Radio::send(const uint8_t *data, uint8_t n) { if (n == 0) return false; uint8_t pa_sel = cfg_.pa_sel; int8_t tx_dbm = cfg_.tx_dbm; for (int attempt = 0; attempt < 6; ++attempt) { 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; { uint8_t duty = 0, hp_max = 0; computePaConfig(pa_sel, tx_dbm, duty, hp_max); const uint8_t p[] = {pa_sel, cfg_.pa_supply, duty, hp_max}; if (!wcmd(OC_SET_PA_CFG, p, sizeof(p))) return false; } { const uint8_t p[] = {static_cast(tx_dbm), TX_RAMP_48US}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) return false; } if (!wcmd(OC_WRITE_BUF8, data, n)) return false; { const uint8_t p[] = {LORA_PKT_HEADER_EXPLICIT, LORA_PKT_PREAMBLE_LEN, 0x00, n, LORA_PKT_CRC_ON, LORA_PKT_IQ_STD}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; } { const uint32_t tx_steps = timeoutMsToRtcSteps(TX_TIMEOUT_MS); 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; } const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(TX_TIMEOUT_MS + TX_POLL_GUARD_MS); while (std::chrono::steady_clock::now() < deadline) { 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) { (void)clearIrq(); (void)setIrqMask(0, 0); return true; } if (irq & IRQ_TIMEOUT) { if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX timeout irq=0x%08X errs=0x%04X\n", irq, getErrors()); (void)clearIrq(); (void)setIrqMask(0, 0); return false; } if (irq & IRQ_LBD) { (void)clearIrq(); (void)setIrqMask(0, 0); if (pa_sel == 0x01) { pa_sel = 0x00; if (tx_dbm > 10) tx_dbm = 10; } else if (tx_dbm > MIN_TX_DBM_FALLBACK) { tx_dbm = static_cast(tx_dbm - 2); } else { if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX fail: LBD at minimum power (%d dBm)\n", static_cast(tx_dbm)); return false; } if (cfg_.verbose) { std::fprintf(stderr, "[lr1121] LBD retry with %s PA %d dBm\n", pa_sel ? "HP" : "LP", static_cast(tx_dbm)); } break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (std::chrono::steady_clock::now() >= deadline) { if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX poll timeout errs=0x%04X\n", getErrors()); (void)clearIrq(); (void)setIrqMask(0, 0); return false; } } return false; } inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info) { 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 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 (;;) { 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 (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); } } } // namespace lr1121