SkyLok/chip_test_example/lr1121_malnus.hpp
2026-05-21 15:27:45 +02:00

553 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lr1121_malnus.hpp — LR1121 LoRa driver, tuned for this exact board
//
// Hardware (hardwired):
// /dev/spidev0.0 CE0=GPIO8, MISO=GPIO9, MOSI=GPIO10, SCK=GPIO11
// GPIO24 LR_DIO0/BUSY
// GPIO25 LR_nRESET
// DIO5 RF switch RFSW0 — driven by chip (HIGH in RX)
// DIO6 RF switch RFSW1 — driven by chip (HIGH in TX)
//
// Crystal oscillator — no TCXO, no SetTcxoMode, no calibration guessing.
// RF switch is configured via SetDioAsRfSwitch (opcode 0x022D) so DIO5/DIO6
// are driven automatically by the chip in the correct state for TX/RX/standby.
#pragma once
#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>
#include <fcntl.h>
#include <linux/gpio.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <unistd.h>
namespace lr1121 {
// ---- Opcodes (SWDR001 confirmed) -------------------------------------------
// system
constexpr uint16_t OC_GET_STATUS = 0x0100;
constexpr uint16_t OC_GET_VERSION = 0x0101; // → [hw,type,fw_hi,fw_lo]
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: freq1, freq2 (× 4 MHz)
constexpr uint16_t OC_SET_DIOIRQ = 0x0113; // 4B dio9_mask + 4B dio8_mask
constexpr uint16_t OC_CLEAR_IRQ = 0x0114;
constexpr uint16_t OC_REBOOT = 0x0118; // 1B: 0=app, 1=stay bootloader
constexpr uint16_t OC_SET_STANDBY = 0x011C; // 1B: 0=RC, 1=XOSC
// regmem / buffer
constexpr uint16_t OC_WRITE_BUF8 = 0x0109;
constexpr uint16_t OC_READ_BUF8 = 0x010A; // 1B offset + 1B len → N bytes
// radio
constexpr uint16_t OC_GET_PKT_STATUS = 0x0204; // LoRa → [rssi,snr,sig_rssi]
constexpr uint16_t OC_GET_RXBUF_STA = 0x0203; // → [payload_len, rx_ptr]
constexpr uint16_t OC_SET_LORA_NET = 0x0208; // 1B: 0=private, 1=LoRaWAN
constexpr uint16_t OC_SET_RX = 0x0209; // 3B timeout RTC steps (32768/s)
constexpr uint16_t OC_SET_TX = 0x020A; // 3B timeout (0=until TxDone)
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; // 1B pwr(int8) + 1B ramp
constexpr uint16_t OC_SET_PKT_ADRS = 0x0212; // 1B tx_base + 1B rx_base
constexpr uint16_t OC_SET_PA_CFG = 0x0215; // pa_sel,pa_supply,duty,hp_max
constexpr uint16_t OC_SET_FALLBACK_MODE = 0x020C; // 1B: 0x01=STBY_RC, 0x02=STBY_XOSC
// RF switch — DIO5/DIO6 driven by chip based on TX/RX/STBY state
constexpr uint16_t OC_SET_DIO_AS_RFSW = 0x022D; // 8B: enable + 7 mode masks
// ---- IRQ bit masks ---------------------------------------------------------
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 = 0x07FF'FFFFu;
// ---- Calibration bitmask ---------------------------------------------------
constexpr uint8_t CALIB_LF_RC = (1 << 0);
constexpr uint8_t CALIB_HF_RC = (1 << 1);
constexpr uint8_t CALIB_PLL = (1 << 2);
constexpr uint8_t CALIB_ADC = (1 << 3);
constexpr uint8_t CALIB_IMAGE = (1 << 4);
constexpr uint8_t CALIB_PLL_TX = (1 << 5);
constexpr uint8_t CALIB_ALL = 0x3F;
// ---- Modulation constants --------------------------------------------------
// SF: 0x05=SF5 .. 0x0C=SF12 (0x07=SF7 is good for short-range tests)
// BW: 0x01=15.6k 0x02=31.2k 0x03=62.5k 0x04=125k 0x05=250k 0x06=500k
// CR: 0x01=4/5 0x02=4/6 0x03=4/7 0x04=4/8
// PA: 0x00=LP (≤15 dBm), 0x01=HP (≤22 dBm) — most modules use HP
constexpr uint32_t FREQ_433 = 433'050'000;
constexpr uint32_t FREQ_868 = 868'000'000;
constexpr uint32_t FREQ_2400 = 2'403'000'000;
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; // LR_DIO0/BUSY
unsigned reset_gpio = 25; // LR_nRESET
uint32_t freq_hz = FREQ_433; // match your antenna
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, 0x01=HP
bool use_dcdc = true;
bool lora_wan = false; // false = private sync word (0x12)
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;
};
class Radio {
public:
bool begin(const Config &cfg);
void end();
// Returns true on TX done, false on timeout.
bool send(const uint8_t *data, uint8_t n);
// Returns bytes received; -1=timeout, -2=CRC error.
int receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms,
RxInfo *rx_info = nullptr);
uint32_t getIrq();
void clearIrq(uint32_t mask);
ChipVersion getVersion();
// Open SPI + GPIOs and hard-reset the chip, skip calibration.
// Useful for sending diagnostic commands if begin() hangs.
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);
void waitBusy();
void hardReset();
bool openGpio(unsigned line, bool out, int &fd_out);
void setGpioLine(int fd, int val);
int getGpioLine(int fd);
static void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2);
static uint8_t computeLDRO(uint8_t sf, uint8_t bw);
};
// ---- Implementation ---------------------------------------------------------
inline void Radio::spiTransfer(uint8_t *buf, size_t n)
{
spi_ioc_transfer tr{};
tr.tx_buf = reinterpret_cast<uint64_t>(buf);
tr.rx_buf = reinterpret_cast<uint64_t>(buf);
tr.len = static_cast<uint32_t>(n);
tr.speed_hz = cfg_.spi_hz;
tr.bits_per_word = 8;
ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr);
}
inline void Radio::wcmd(uint16_t op, const uint8_t *p, size_t n)
{
waitBusy();
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();
}
inline void Radio::rcmd(uint16_t op, const uint8_t *params, size_t np,
uint8_t *out, size_t nr)
{
waitBusy();
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);
waitBusy();
uint8_t rbuf[260]{};
spiTransfer(rbuf, nr + 1); // byte 0 is a dummy status
std::memcpy(out, rbuf + 1, nr);
}
// GetStatus: the LR1121 outputs [stat1, stat2, irq31:24, irq23:16, irq15:8, irq7:0]
// on the first 6 MISO bytes of ANY SPI transaction — regardless of what opcode is sent.
// (Confirmed in RadioLib source: it sends 6 null bytes with no opcode and reads buff[2..5].)
// Does NOT call waitBusy() — designed to be polled freely during TX/RX.
inline uint32_t Radio::getIrq()
{
uint8_t b[6] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
spiTransfer(b, 6);
// b[0]=stat1, b[1]=stat2, b[2..5]=irq[31:24..7:0]
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 void Radio::waitBusy()
{
for (int i = 0; i < 200'000; ++i) {
if (!getGpioLine(busy_fd_)) return;
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
if (cfg_.verbose)
std::fprintf(stderr, "[lr1121] waitBusy TIMEOUT after 10s — chip hung?\n");
}
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 void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2)
{
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 { lo = 900; hi = 928; }
f1 = (uint8_t)(lo / 4);
f2 = (uint8_t)((hi + 3) / 4);
}
inline uint8_t Radio::computeLDRO(uint8_t sf, uint8_t bw)
{
static const uint32_t bw_khz[] = { 0, 15625, 31250, 62500, 125000,
250000, 500000 };
uint32_t bw_hz = (bw < 7) ? bw_khz[bw] : 125000;
uint32_t sym_ms = (1u << sf) * 1000u / bw_hz;
return (sym_ms > 16) ? 1 : 0;
}
inline bool Radio::begin(const Config &c)
{
cfg_ = 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);
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 chip type 0x%02X (want 0x03=LR1121)\n",
ver.type);
return false;
}
#define LR_STEP(msg) do { if (c.verbose) std::fprintf(stderr, "[lr1121] -> " msg "\n"); } while(0)
LR_STEP("SetStandby(RC)");
{ const uint8_t a[] = {0x00}; wcmd(OC_SET_STANDBY, a, 1); }
// Crystal oscillator — no TCXO, no SetTcxoMode.
// Calibrate from RC clock (required), then switch to XOSC standby so the
// crystal stays running during radio configuration and TX/RX ramp-up.
// Without STBY_XOSC here the PA cold-starts the crystal on every TX and
// silently fails to ramp up.
LR_STEP("Calibrate(ALL)");
{ const uint8_t a[] = {CALIB_ALL}; wcmd(OC_CALIBRATE, a, 1); }
LR_STEP("ClearErrors");
wcmd(OC_CLEAR_ERRORS);
LR_STEP("SetStandby(XOSC)");
{ const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); } // 0x01 = XOSC, not RC
#undef LR_STEP
// --- 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); }
if (f < 1'000'000'000u) {
uint8_t f1, f2; imgCalFreqs(f, 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); }
// pa_supply=0x00 → internal DC-DC regulator (correct for most modules).
// 0x01 = VBAT direct — only needed on specific high-power reference designs.
{ const uint8_t a[] = {c.pa_sel, 0x00, 0x04, 0x07};
wcmd(OC_SET_PA_CFG, a, 4); }
{ 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); }
// RF switch via SetDioAsRfSwitch (0x022D).
// enable=0x03 → DIO5 and DIO6 are RF switch outputs.
// Bit 0 = DIO5 (RFSW0), bit 1 = DIO6 (RFSW1).
// standby: both LOW | rx: DIO5=HIGH, DIO6=LOW | tx/tx_hp: DIO5=LOW, DIO6=HIGH
{ const uint8_t a[] = { 0x03, // enable: DIO5 + DIO6
0x00, // standby: both LOW
0x01, // rx: DIO5=HIGH
0x02, // tx: DIO6=HIGH
0x02, // tx_hp: DIO6=HIGH
0x00, // tx_hf: both LOW (2.4 GHz path unused)
0x00, // gnss: both LOW
0x00}; // wifi: both LOW
wcmd(OC_SET_DIO_AS_RFSW, a, 8);
if (c.verbose) std::fprintf(stderr, "[lr1121] RF switch configured (DIO5/DIO6)\n"); }
// After TX/RX (or an aborted TX due to EOL), fall back to STBY_RC so the
// chip is in a known state. Without this the fallback is undefined and
// subsequent SetStandby + SetTx sequences can be silently ignored.
{ const uint8_t a[] = {0x01}; wcmd(OC_SET_FALLBACK_MODE, a, 1); }
const uint8_t irq_zero[8]{};
wcmd(OC_SET_DIOIRQ, irq_zero, 8);
clearIrq(IRQ_ALL);
if (c.verbose)
std::fprintf(stderr, "[lr1121] init OK: %u Hz, SF%u, PA=%s, crystal osc\n",
c.freq_hz, c.sf, c.pa_sel ? "HP" : "LP");
return true;
}
inline bool Radio::beginRaw(const Config &c)
{
cfg_ = 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);
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)
{
// Return chip to XOSC standby and clear any prior error/IRQ state before TX.
{ const uint8_t a[] = {0x01}; wcmd(OC_SET_STANDBY, a, 1); }
wcmd(OC_CLEAR_ERRORS);
wcmd(OC_WRITE_BUF8, data, n);
{ const uint8_t a[] = {0x00, 0x08, 0x00, n, 0x01, 0x00};
wcmd(OC_SET_PKT_PARAM, a, 6); }
// Enable TX_DONE and TIMEOUT on DIO9 — matches RadioLib's startTransmit().
// IRQ register is updated regardless, but this also lets DIO9 signal completion.
{ const uint8_t a[] = {0x00, 0x00, 0x04, 0x04, // DIO9: TX_DONE(bit2)|TIMEOUT(bit10)
0x00, 0x00, 0x00, 0x00}; // DIO8: none
wcmd(OC_SET_DIOIRQ, a, 8); }
clearIrq(IRQ_ALL);
{ const uint8_t a[] = {0x00, 0x00, 0x00}; wcmd(OC_SET_TX, a, 3); }
for (int i = 0; i < 50'000; ++i) {
uint32_t irq = getIrq();
if (irq & IRQ_TX_DONE) {
clearIrq(IRQ_ALL);
// Restore DIO IRQ mask to silent.
const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8);
return true;
}
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
// Read stat bytes alongside IRQ for diagnosis.
uint8_t sb[6] = {}; spiTransfer(sb, 6);
uint32_t irq_now = ((uint32_t)sb[2]<<24)|((uint32_t)sb[3]<<16)|((uint32_t)sb[4]<<8)|sb[5];
if (cfg_.verbose)
std::fprintf(stderr, "[lr1121] send: TX timeout stat1=0x%02X stat2=0x%02X irq=0x%08X\n",
sb[0], sb[1], irq_now);
const uint8_t z[8]{}; wcmd(OC_SET_DIOIRQ, z, 8);
clearIrq(IRQ_ALL);
{ 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); }
// Enable RX_DONE(bit3), TIMEOUT(bit10), CRC_ERR(bit7) on DIO9 — matches RadioLib.
{ const uint8_t a[] = {0x00, 0x00, 0x04, 0x88, // DIO9: RX_DONE|TIMEOUT|CRC_ERR
0x00, 0x00, 0x00, 0x00};
wcmd(OC_SET_DIOIRQ, a, 8); }
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) {
bool crc_err = irq & IRQ_CRC_ERR;
clearIrq(IRQ_ALL);
if (rx_info) {
uint8_t ps[3]{};
rcmd(OC_GET_PKT_STATUS, nullptr, 0, ps, 3);
rx_info->rssi_dbm = -(int8_t)(ps[0] >> 1);
rx_info->snr_db = ((int8_t)ps[1] + 2) >> 2;
rx_info->signal_rssi_dbm = -(int8_t)(ps[2] >> 1);
}
if (crc_err) 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);
return len;
}
if (irq & IRQ_TIMEOUT) { clearIrq(IRQ_ALL); return -1; }
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
} // namespace lr1121