sloppified ai example but working, yaaaaaay, :,)

This commit is contained in:
shinya 2026-05-28 10:42:05 +02:00
parent 45a60d64b9
commit 1d3d25e6ae
5 changed files with 472 additions and 471 deletions

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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 <chrono>
@ -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;
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;
// ── 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 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 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 openSpi(const Config &cfg);
bool openGpio(unsigned line, bool out, int &fd_out);
void setGpioLine(int fd, int val);
int getGpioLine(int fd);
bool openSpi(const Config &c);
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);
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();
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);
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<uint64_t>(buf);
tr.rx_buf = reinterpret_cast<uint64_t>(buf);
tr.len = static_cast<uint32_t>(n);
tr.len = static_cast<uint32_t>(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<uint8_t>(op >> 8);
cmd[1] = static_cast<uint8_t>(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<uint8_t>(op >> 8);
cmd[1] = static_cast<uint8_t>(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<uint8_t>(irq1 >> 24), static_cast<uint8_t>(irq1 >> 16),
static_cast<uint8_t>(irq1 >> 8), static_cast<uint8_t>(irq1),
static_cast<uint8_t>(irq2 >> 24), static_cast<uint8_t>(irq2 >> 16),
static_cast<uint8_t>(irq2 >> 8), static_cast<uint8_t>(irq2),
};
return wcmd(OC_SET_DIOIRQ, p, sizeof(p));
}
inline bool Radio::clearIrq(uint32_t mask)
{
const uint8_t p[4] = {
static_cast<uint8_t>(mask >> 24), static_cast<uint8_t>(mask >> 16),
static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(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<uint32_t>(b[2]) << 24) |
(static_cast<uint32_t>(b[3]) << 16) |
(static_cast<uint32_t>(b[4]) << 8) |
(static_cast<uint32_t>(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<uint16_t>((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.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<uint8_t>(lo / 4);
f2 = static_cast<uint8_t>((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<uint64_t>(ms) * 32768u) / 1000u;
return (steps > 0x00FFFFFFu) ? 0x00FFFFFFu : static_cast<uint32_t>(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);
}
#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;
}
if (v.type != 0x03) return false;
wcmd(OC_CLEAR_ERRORS);
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); }
{ const uint8_t p[] = {static_cast<uint8_t>(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<uint8_t>(cfg_.freq_hz >> 24), static_cast<uint8_t>(cfg_.freq_hz >> 16),
static_cast<uint8_t>(cfg_.freq_hz >> 8), static_cast<uint8_t>(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<int>(cfg_.tx_dbm));
}
}
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.tx_dbm), 0x02}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) return false; }
{ const uint8_t p[] = {static_cast<uint8_t>(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<uint8_t>(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 (reset_fd_ >= 0) { ::close(reset_fd_); reset_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)
{
// 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<uint8_t>(tx_steps >> 16),
static_cast<uint8_t>(tx_steps >> 8),
static_cast<uint8_t>(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));
}
// Timeout — print diagnostics then clean up.
if (irq & (IRQ_TIMEOUT | IRQ_LBD)) {
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");
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));
}
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<uint8_t>(steps >> 16),
static_cast<uint8_t>(steps >> 8),
static_cast<uint8_t>(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 (std::chrono::steady_clock::now() >= deadline) {
(void)clearIrq();
(void)setIrqMask(0, 0);
return -1;
}
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; }
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<int8_t>(-(int)p[0] / 2);
rx_info->snr_db = static_cast<int8_t>(static_cast<int8_t>(p[1]) / 4);
rx_info->signal_rssi_dbm = static_cast<int8_t>(-(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<int>(len);
}
}

32
test/flake.nix Normal file
View File

@ -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"
'';
};
};
}