Compare commits

..

No commits in common. "b86c441045867602bf8a8d833415fe0289463799" and "3a616b512a5d1a143712e86cc0aa4c61c0cdae9f" have entirely different histories.

7 changed files with 404 additions and 580 deletions

View File

@ -8,22 +8,9 @@ Pi Zero W 2 captures frames, runs a motion shader, sends diffs over LoRa.
- `chip_test_example/` — LR1121 LoRa + ICM-20948 IMU + u-blox GPS drivers/tests.
- `main.cpp` — prototype that wires camera + shader + TCP together.
## Goal
Automatic, real-time detection of fast high-flying objects (planes, drones) using
multiple Pi cameras and a GLES motion shader. Objects moving in straight lines or
smooth curves are kept; everything else is filtered out.
Each node scans the sky, detects motion, extracts a vertex line from the diff frame,
timestamps it via GPS, and transmits the minimal payload over LoRa.
The central device is also a Pi camera node running the same base pipeline, with
additional functionality for receiving LoRa packets from other nodes, fusing
sightlines, and saving combined results locally.
## Status
The examples are all individually working. Final integration (camera → shader → math →
LoRa) is the next step.
References:
- https://www.youtube.com/watch?v=zFiubdrJqqI
- https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector/
## Build main.cpp
@ -33,8 +20,3 @@ g++ main.cpp -o main \
-levent -levent_pthreads -lpthread \
-lturbojpeg -lglfw -lGLEW -lGL -lEGL -lGLESv2
```
## References
- https://www.youtube.com/watch?v=zFiubdrJqqI
- https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector/

View File

@ -61,7 +61,7 @@ make
Run verbose first so you see exactly where it fails:
```sh
sudo ./lora_rx -v
sudo ./lora_rx -v --433
sudo ./imu_test -v
./gps_test -v
```
@ -72,7 +72,7 @@ sudo ./imu_test -v
```sh
ls /dev/spidev0.0 # SPI enabled?
sudo ./lora_rx -v # step labels show exactly which command hangs
sudo ./lora_rx -v --433 # step labels show exactly which command hangs
```
If it hangs at `Calibrate` — the chip isn't responding over SPI at all.
@ -81,6 +81,13 @@ Check wiring, CS, and that SPI is enabled. The crystal needs no tuning.
If TX/RX runs but packets never arrive — check that DIO5/DIO6 reach the RF
switch on your module. Without the switch, the antenna path is disconnected.
To try 2.4 GHz instead (different antenna required):
```sh
sudo ./lora_rx -v --24
sudo ./lora_tx -v --24
```
If the chip is stuck in bootloader (fw < 0x02xx), escape with:
```sh

View File

@ -39,10 +39,10 @@ deploy_and_build() {
return $BUILD_STATUS
}
deploy_and_build 10.91.51.183 &
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
@ -52,7 +52,7 @@ wait $PID2
STATUS2=$?
echo "----------------------------------------"
echo "10.91.51.183 status: $STATUS1"
echo "10.91.51.165 status: $STATUS1"
echo "10.91.51.166 status: $STATUS2"
if [ $STATUS1 -ne 0 ] || [ $STATUS2 -ne 0 ]; then

View File

@ -1,110 +1,93 @@
#include <cstdio>
#include <csignal>
#include <cstring>
#include "lr1121_malnus.hpp"
static volatile std::sig_atomic_t g_stop = 0;
static void onSignal(int)
{
g_stop = 1;
}
static void applyPaPreset(lr1121::Config &cfg, bool hp_mode)
{
cfg.pa_sel = hp_mode ? lr1121::PA_HP : lr1121::PA_LP;
cfg.tx_dbm = hp_mode ? 14 : 8;
cfg.pa_sel = hp_mode ? 0x01 : 0x00;
cfg.tx_dbm = hp_mode ? 14 : 10;
}
static bool parseArgs(int argc, char **argv, bool &verbose, bool &do_reset, bool &hp_mode)
{
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-v") == 0) verbose = true;
else if (std::strcmp(argv[i], "--reset") == 0) do_reset = true;
else if (std::strcmp(argv[i], "--hp") == 0) hp_mode = true;
else if (std::strcmp(argv[i], "--lp") == 0) hp_mode = false;
else {
std::fprintf(stderr, "Unknown option: %s\n"
"Usage: sudo ./lora_rx [-v] [--reset] [--lp|--hp]\n", argv[i]);
return false;
if (std::strcmp(argv[i], "-v") == 0) {
verbose = true;
continue;
}
if (std::strcmp(argv[i], "--reset") == 0) {
do_reset = true;
continue;
}
if (std::strcmp(argv[i], "--hp") == 0) {
hp_mode = true;
continue;
}
if (std::strcmp(argv[i], "--lp") == 0) {
hp_mode = false;
continue;
}
std::fprintf(stderr, "Unknown option: %s\nUsage: sudo ./lora_rx [-v] [--reset] [--lp|--hp]\n", argv[i]);
return false;
}
return true;
}
int main(int argc, char **argv)
{
std::signal(SIGINT, onSignal);
std::signal(SIGTERM, onSignal);
lr1121::Config cfg;
cfg.freq_hz = lr1121::FREQ_433;
cfg.busy_gpio = 24;
cfg.reset_gpio = 25;
cfg.dio9_gpio = 4;
cfg.sf = 0x07;
cfg.bw = 0x04;
cfg.cr = 0x01;
constexpr uint32_t rx_timeout_ms = 1000;
bool verbose = false;
bool do_reset = false;
bool hp_mode = false;
if (!parseArgs(argc, argv, verbose, do_reset, hp_mode)) return 2;
if (!parseArgs(argc, argv, cfg.verbose, do_reset, hp_mode)) return 2;
applyPaPreset(cfg, hp_mode);
std::printf("RX %u Hz, tmo=%ums, gpio(busy=%u reset=%u dio9=%u)%s\n",
cfg.freq_hz, rx_timeout_ms,
cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio,
verbose ? " [v]" : "");
cfg.freq_hz, rx_timeout_ms, cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio, cfg.verbose ? " [v]" : "");
lr1121::Radio radio;
if (!radio.begin(cfg)) {
std::fprintf(stderr, "ERROR: radio init failed: %s\n",
lr1121::Radio::errorString(radio.lastError()));
std::fprintf(stderr, "ERROR: radio init failed\n");
return 1;
}
if (verbose) {
const auto v = radio.chipVersion();
std::printf("[lr1121] hw=0x%02X type=0x%02X fw=0x%02X%02X vbat=%.2fV\n",
v.hw, v.type, v.fw_hi, v.fw_lo, radio.vbatVolts());
}
if (do_reset) {
std::puts("Applying soft settings reset...");
if (!radio.softResetSettings()) {
std::fprintf(stderr, "ERROR: soft settings reset failed: %s\n",
lr1121::Radio::errorString(radio.lastError()));
std::fprintf(stderr, "ERROR: soft settings reset failed\n");
return 1;
}
}
if (!radio.startListening()) {
std::fprintf(stderr, "ERROR: start listening failed: %s\n",
lr1121::Radio::errorString(radio.lastError()));
return 1;
}
std::puts("Radio OK - listening (continuous RX). Press Ctrl+C to stop.");
std::puts("Radio OK - listening...");
uint8_t buf[256];
int pkt = 0;
uint32_t timeout_total = 0;
uint32_t crc_total = 0;
while (!g_stop) {
for (;;) {
lr1121::RxInfo info{};
const int r = radio.receive(buf, uint8_t(sizeof(buf) - 1), rx_timeout_ms, &info);
const int r = radio.receive(buf, static_cast<uint8_t>(sizeof(buf) - 1), rx_timeout_ms, &info);
if (r > 0) {
buf[r] = '\0';
std::printf("[%04d] %3d B rssi=%4d snr=%3d '%s'\n",
++pkt, r, info.rssi_dbm, info.snr_db, buf);
} else if (r == -2) {
std::printf("crc error x%u\n", ++crc_total);
} else if (radio.lastError() == lr1121::Error::RxTimeout) {
} else if (r == -1) {
++timeout_total;
if ((timeout_total % 10u) == 0u)
std::printf("timeout x%u\n", timeout_total);
if ((timeout_total % 10u) == 0u) std::printf("timeout x%u\n", timeout_total);
} else if (r == -2) {
++crc_total;
std::printf("crc error x%u\n", crc_total);
} else {
std::printf("rx error: %s\n",
lr1121::Radio::errorString(radio.lastError()));
std::printf("rx code=%d\n", r);
}
}
std::puts("\nStopping listener...");
radio.stopListening();
radio.end();
std::puts("Radio OK - stopped listening...");
return 0;
}

View File

@ -7,22 +7,31 @@
static void applyPaPreset(lr1121::Config &cfg, bool hp_mode)
{
cfg.pa_sel = hp_mode ? lr1121::PA_HP : lr1121::PA_LP;
cfg.tx_dbm = hp_mode ? 14 : 8;
cfg.pa_sel = hp_mode ? 0x01 : 0x00;
cfg.tx_dbm = hp_mode ? 14 : 10;
}
static bool parseArgs(int argc, char **argv, bool &verbose, bool &do_reset, bool &hp_mode)
{
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-v") == 0) verbose = true;
else if (std::strcmp(argv[i], "--reset") == 0) do_reset = true;
else if (std::strcmp(argv[i], "--hp") == 0) hp_mode = true;
else if (std::strcmp(argv[i], "--lp") == 0) hp_mode = false;
else {
std::fprintf(stderr, "Unknown option: %s\n"
"Usage: sudo ./lora_tx [-v] [--reset] [--lp|--hp]\n", argv[i]);
return false;
if (std::strcmp(argv[i], "-v") == 0) {
verbose = true;
continue;
}
if (std::strcmp(argv[i], "--reset") == 0) {
do_reset = true;
continue;
}
if (std::strcmp(argv[i], "--hp") == 0) {
hp_mode = true;
continue;
}
if (std::strcmp(argv[i], "--lp") == 0) {
hp_mode = false;
continue;
}
std::fprintf(stderr, "Unknown option: %s\nUsage: sudo ./lora_tx [-v] [--reset] [--lp|--hp]\n", argv[i]);
return false;
}
return true;
}
@ -30,34 +39,32 @@ static bool parseArgs(int argc, char **argv, bool &verbose, bool &do_reset, bool
int main(int argc, char **argv)
{
lr1121::Config cfg;
bool verbose = false;
cfg.freq_hz = lr1121::FREQ_433;
cfg.busy_gpio = 24;
cfg.reset_gpio = 25;
cfg.dio9_gpio = 4;
cfg.sf = 0x07;
cfg.bw = 0x04;
cfg.cr = 0x01;
bool do_reset = false;
bool hp_mode = false;
if (!parseArgs(argc, argv, verbose, do_reset, hp_mode)) return 2;
if (!parseArgs(argc, argv, cfg.verbose, do_reset, hp_mode)) return 2;
applyPaPreset(cfg, hp_mode);
std::printf("TX %u Hz, %s PA, %d dBm, gpio(busy=%u reset=%u dio9=%u)%s\n",
cfg.freq_hz, cfg.pa_sel == lr1121::PA_HP ? "HP" : "LP",
int(cfg.tx_dbm), cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio,
verbose ? " [v]" : "");
cfg.freq_hz, cfg.pa_sel ? "HP" : "LP", static_cast<int>(cfg.tx_dbm),
cfg.busy_gpio, cfg.reset_gpio, cfg.dio9_gpio, cfg.verbose ? " [v]" : "");
lr1121::Radio radio;
if (!radio.begin(cfg)) {
std::fprintf(stderr, "ERROR: radio init failed: %s\n",
lr1121::Radio::errorString(radio.lastError()));
std::fprintf(stderr, "ERROR: radio init failed\n");
return 1;
}
if (verbose) {
const auto v = radio.chipVersion();
std::printf("[lr1121] hw=0x%02X type=0x%02X fw=0x%02X%02X vbat=%.2fV\n",
v.hw, v.type, v.fw_hi, v.fw_lo, radio.vbatVolts());
}
if (do_reset) {
std::puts("Applying soft settings reset...");
if (!radio.softResetSettings()) {
std::fprintf(stderr, "ERROR: soft settings reset failed: %s\n",
lr1121::Radio::errorString(radio.lastError()));
std::fprintf(stderr, "ERROR: soft settings reset failed\n");
return 1;
}
}
@ -66,14 +73,8 @@ int main(int argc, char **argv)
for (int n = 0; ; ++n) {
char msg[32];
const int len = std::snprintf(msg, sizeof(msg), "hello %d", n);
const bool ok = radio.send(reinterpret_cast<const uint8_t *>(msg),
uint8_t(len));
if (ok) {
std::printf("[%04d] %s: OK\n", n, msg);
} else {
std::printf("[%04d] %s: FAIL (%s)\n", n, msg,
lr1121::Radio::errorString(radio.lastError()));
}
const bool ok = radio.send(reinterpret_cast<const uint8_t *>(msg), static_cast<uint8_t>(len));
std::printf("[%04d] %s: %s\n", n, msg, ok ? "OK" : "FAIL");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

View File

@ -1,18 +1,21 @@
// lr1121_malnus.hpp — minimal LR1121 LoRa driver (Linux, header-only).
// lr1121_malnus.hpp — minimal LR1121 LoRa driver for this board
//
// Defaults: SPI /dev/spidev0.0, BUSY GPIO24, RESET GPIO25, DIO9 GPIO4.
// RF switch is driven by the chip via DIO5/DIO6.
// Board wiring:
// SPI : /dev/spidev0.0 (NSS GPIO8)
// BUSY : GPIO24
// RESET: GPIO25
// RF switch controlled by LR1121 DIO5/DIO6
//
// The driver never logs — failures return false (or a negative code); read
// lastError()/errorString() for the reason. Callers decide what to print.
//
// Refs: Semtech LR1121 user manual rev 1.2, SWDR001, RadioLib LR11x0.
// Command set references:
// - Semtech LR1121 user manual rev 1.2
// - Semtech SWDR001
// - RadioLib LR11x0 behavior
#pragma once
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <thread>
#include <fcntl.h>
@ -23,70 +26,77 @@
namespace lr1121 {
// Opcodes (Semtech LR1121 user manual rev 1.2).
constexpr uint16_t
OC_GET_VERSION = 0x0101, OC_WRITE_BUF8 = 0x0109,
OC_READ_BUF8 = 0x010A, OC_CLEAR_RXBUF = 0x010B,
OC_GET_ERRORS = 0x010D, OC_CLEAR_ERRORS = 0x010E,
OC_CALIBRATE = 0x010F, OC_SET_REGMODE = 0x0110,
OC_CALIBRATE_IMG = 0x0111, OC_SET_DIO_AS_RFSW = 0x0112,
OC_SET_DIOIRQ = 0x0113, OC_CLEAR_IRQ = 0x0114,
OC_GET_IRQ = 0x0115, OC_REBOOT = 0x0118,
OC_GET_VBAT = 0x0119, OC_SET_STANDBY = 0x011C,
OC_GET_RXBUF_STA = 0x0203, OC_GET_PKT_STATUS = 0x0204,
OC_SET_LORA_NET = 0x0208, OC_SET_RX = 0x0209,
OC_SET_TX = 0x020A, OC_SET_RF_FREQ = 0x020B,
OC_SET_PKT_TYPE = 0x020E, OC_SET_MOD_PARAM = 0x020F,
OC_SET_PKT_PARAM = 0x0210, OC_SET_TX_PARAMS = 0x0211,
OC_SET_PKT_ADRS = 0x0212, OC_SET_FALLBACK_MODE = 0x0213,
OC_SET_PA_CFG = 0x0215;
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, IRQ_RX_DONE = 1u << 3,
IRQ_CRC_ERR = 1u << 7, IRQ_TIMEOUT = 1u << 10,
IRQ_LBD = 1u << 21, IRQ_ALL = 0x1BF80FFCu;
constexpr uint32_t FREQ_433 = 433'050'000u,
FREQ_868 = 868'000'000u,
FREQ_2400 = 2'403'000'000u;
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 uint8_t PA_LP = 0x00, PA_HP = 0x01, PKT_TYPE_LORA = 0x02;
constexpr uint8_t LORA_HEADER_EXPLICIT = 0x00, LORA_CRC_ON = 0x01, LORA_IQ_STD = 0x00;
constexpr uint16_t LORA_PREAMBLE_LEN = 8;
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 uint32_t TX_TIMEOUT_MS = 3000, TX_POLL_GUARD_MS = 500;
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;
enum class Error : uint8_t {
Ok = 0,
NotReady,
SpiOpen,
SpiIo,
GpioOpen,
BusyTimeout,
BadChip,
TxTimeout,
TxLbd,
RxTimeout,
RxCrc,
};
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;
unsigned dio9_gpio = 4; // IRQ line from LR1121
uint32_t freq_hz = FREQ_433;
uint8_t sf = 7;
uint8_t bw = 0x04; // 125 kHz
uint8_t cr = 0x01; // 4/5
uint8_t sf = 0x07;
uint8_t bw = 0x04;
uint8_t cr = 0x01;
int8_t tx_dbm = 10;
uint8_t pa_sel = PA_LP; // LP avoids LBD on weak VBAT
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 {
@ -96,10 +106,10 @@ struct RxInfo {
};
struct ChipVersion {
uint8_t hw = 0;
uint8_t type = 0;
uint8_t fw_hi = 0;
uint8_t fw_lo = 0;
uint8_t hw;
uint8_t type;
uint8_t fw_hi;
uint8_t fw_lo;
};
class Radio {
@ -109,76 +119,44 @@ public:
bool softResetSettings();
void end();
// Returns true on TX_DONE, false otherwise; check lastError() for reason.
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);
// One-shot RX: arm, wait timeout_ms, return to standby.
// Returns bytes received (>=0), -1 on timeout/error, -2 on CRC error.
int receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms,
RxInfo *rx_info = nullptr);
// Continuous RX: arm once, call receive() in a loop — chip stays in RX
// between packets. timeout_ms in receive() becomes a per-call poll window
// (0 = wait forever). Call stopListening() to return to standby.
bool startListening();
void stopListening();
// Runtime tuning. Call from standby (the driver leaves the chip in
// standby_xosc after begin()/send()/receive()).
bool setFrequency(uint32_t hz);
bool setTxPower(int8_t dbm, uint8_t pa_sel = PA_LP);
bool setModulation(uint8_t sf, uint8_t bw, uint8_t cr);
// Diagnostics — read on demand, never side-effects.
ChipVersion chipVersion();
uint16_t chipErrors();
uint8_t vbatRaw();
float vbatVolts() { return vbatRaw() / 56.4f; }
const Config &config() const { return cfg_; }
Error lastError() const { return last_err_; }
static const char *errorString(Error e);
ChipVersion getVersion();
void reboot(bool stay_in_bootloader = false);
private:
Config cfg_{};
int spi_fd_ = -1, busy_fd_ = -1, reset_fd_ = -1, dio9_fd_ = -1;
bool listening_ = false;
Error last_err_ = Error::NotReady;
int spi_fd_ = -1;
int busy_fd_ = -1;
int reset_fd_ = -1;
int dio9_fd_ = -1;
bool fail(Error e) { last_err_ = e; return false; }
bool openSpi();
bool openSpi(const Config &cfg);
bool openGpio(unsigned line, bool out, int &fd_out);
int readGpio(int fd);
void writeGpio(int fd, int value);
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 wcmd(uint16_t op, std::initializer_list<uint8_t> il);
bool rcmd(uint16_t op, const uint8_t *params, size_t np,
uint8_t *out, size_t nr);
bool rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr);
bool setStandbyXosc() { return wcmd(OC_SET_STANDBY, {0x01}); }
bool setStandbyRc() { return wcmd(OC_SET_STANDBY, {0x00}); }
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();
bool writePktParam(uint8_t payload_len);
bool writePaCfg(uint8_t pa_sel, int8_t dbm);
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);
static void computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max);
};
// ---------- SPI / command primitives ----------
inline bool Radio::spiTransfer(uint8_t *buf, size_t len)
{
spi_ioc_transfer tr{};
@ -193,74 +171,94 @@ inline bool Radio::spiTransfer(uint8_t *buf, size_t len)
inline bool Radio::wcmd(uint16_t op, const uint8_t *params, size_t n)
{
if (n > 256) return false;
if (!waitBusy()) return fail(Error::BusyTimeout);
if (!waitBusy()) return false;
uint8_t cmd[258]{};
cmd[0] = uint8_t(op >> 8);
cmd[1] = uint8_t(op);
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 fail(Error::SpiIo);
if (!waitBusy()) return fail(Error::BusyTimeout);
return true;
if (!spiTransfer(cmd, 2 + n)) return false;
return waitBusy();
}
inline bool Radio::wcmd(uint16_t op, std::initializer_list<uint8_t> il)
{
return wcmd(op, il.begin(), il.size());
}
inline bool 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 (np > 30 || nr > 259) return false;
if (!waitBusy()) return fail(Error::BusyTimeout);
if (!waitBusy()) return false;
uint8_t cmd[32]{};
cmd[0] = uint8_t(op >> 8);
cmd[1] = uint8_t(op);
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 fail(Error::SpiIo);
if (!waitBusy()) return fail(Error::BusyTimeout);
if (!spiTransfer(cmd, 2 + np)) return false;
if (!waitBusy()) return false;
uint8_t rsp[260]{};
if (!spiTransfer(rsp, nr + 1)) return fail(Error::SpiIo);
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)
{
return wcmd(OC_SET_DIOIRQ, {
uint8_t(irq1 >> 24), uint8_t(irq1 >> 16), uint8_t(irq1 >> 8), uint8_t(irq1),
uint8_t(irq2 >> 24), uint8_t(irq2 >> 16), uint8_t(irq2 >> 8), uint8_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)
{
return wcmd(OC_CLEAR_IRQ, {
uint8_t(mask >> 24), uint8_t(mask >> 16), uint8_t(mask >> 8), uint8_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));
}
inline uint32_t Radio::getIrq()
{
uint8_t b[4]{};
if (!rcmd(OC_GET_IRQ, nullptr, 0, b, sizeof(b))) return 0;
return (uint32_t(b[0]) << 24) | (uint32_t(b[1]) << 16) |
(uint32_t(b[2]) << 8) | uint32_t(b[3]);
return (static_cast<uint32_t>(b[0]) << 24) |
(static_cast<uint32_t>(b[1]) << 16) |
(static_cast<uint32_t>(b[2]) << 8) |
static_cast<uint32_t>(b[3]);
}
// ---------- SPI / GPIO open ----------
inline bool Radio::openSpi()
inline uint16_t Radio::getErrors()
{
spi_fd_ = ::open(cfg_.spi_path, O_RDWR);
if (spi_fd_ < 0) return fail(Error::SpiOpen);
uint8_t e[2]{};
if (!rcmd(OC_GET_ERRORS, nullptr, 0, e, sizeof(e))) return 0xFFFF;
return static_cast<uint16_t>((e[0] << 8) | e[1]);
}
uint8_t mode = SPI_MODE_0, bits = 8;
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 fail(Error::SpiOpen);
ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg.spi_hz) < 0) {
::close(spi_fd_);
spi_fd_ = -1;
return false;
}
return true;
}
@ -268,7 +266,7 @@ inline bool Radio::openSpi()
inline bool Radio::openGpio(unsigned line, bool out, int &fd_out)
{
int chip = ::open(cfg_.gpio_chip, O_RDWR);
if (chip < 0) return fail(Error::GpioOpen);
if (chip < 0) return false;
gpiohandle_request req{};
req.lineoffsets[0] = line;
@ -276,22 +274,22 @@ inline bool Radio::openGpio(unsigned line, bool out, int &fd_out)
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 fail(Error::GpioOpen);
if (rc < 0) return false;
fd_out = req.fd;
return true;
}
inline int Radio::readGpio(int fd)
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::writeGpio(int fd, int value)
inline void Radio::setGpioLine(int fd, int value)
{
gpiohandle_data d{};
d.values[0] = value;
@ -300,9 +298,9 @@ inline void Radio::writeGpio(int fd, int value)
inline void Radio::hardReset()
{
writeGpio(reset_fd_, 0);
setGpioLine(reset_fd_, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
writeGpio(reset_fd_, 1);
setGpioLine(reset_fd_, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
@ -310,46 +308,49 @@ inline bool Radio::waitBusy(int timeout_ms)
{
const int loops = timeout_ms * 20;
for (int i = 0; i < loops; ++i) {
if (readGpio(busy_fd_) == 0) return true;
if (getGpioLine(busy_fd_) == 0) return true;
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
return false;
}
// ---------- Math helpers ----------
inline void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2)
{
const uint32_t mhz = hz / 1'000'000u;
uint32_t lo, hi;
// 2.4 GHz values are hardcoded (lo/4 would overflow uint8_t).
if (mhz >= 2000) { f1 = 0xD7; f2 = 0xDB; return; }
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 = 902; hi = 928; }
f1 = uint8_t(lo / 4);
f2 = uint8_t((hi + 3) / 4);
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)
{
static const uint32_t bw_hz[] = {0, 15625, 31250, 62500, 125000, 250000, 500000};
const uint32_t bw_val = (bw < 7) ? bw_hz[bw] : 125000;
return ((1000u << sf) / bw_val) > 16 ? 1 : 0;
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 = (uint64_t(ms) * 32768u) / 1000u;
return steps > 0x00FFFFFFu ? 0x00FFFFFFu : uint32_t(steps);
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)
inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max)
{
if (pa_sel == PA_HP) {
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; }
@ -365,153 +366,108 @@ inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm,
}
}
inline bool Radio::writePktParam(uint8_t payload_len)
{
return wcmd(OC_SET_PKT_PARAM, {
uint8_t(LORA_PREAMBLE_LEN >> 8),
uint8_t(LORA_PREAMBLE_LEN),
LORA_HEADER_EXPLICIT,
payload_len,
LORA_CRC_ON,
LORA_IQ_STD,
});
}
inline bool Radio::writePaCfg(uint8_t pa_sel, int8_t dbm)
{
uint8_t duty = 0, hp_max = 0;
computePaConfig(pa_sel, dbm, duty, hp_max);
return wcmd(OC_SET_PA_CFG, {pa_sel, cfg_.pa_supply, duty, hp_max});
}
// ---------- Diagnostics ----------
inline ChipVersion Radio::chipVersion()
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 uint16_t Radio::chipErrors()
{
uint8_t e[2]{};
if (!rcmd(OC_GET_ERRORS, nullptr, 0, e, sizeof(e))) return 0xFFFF;
return uint16_t((e[0] << 8) | e[1]);
}
inline uint8_t Radio::vbatRaw()
{
uint8_t raw = 0;
(void)rcmd(OC_GET_VBAT, nullptr, 0, &raw, 1);
return raw;
}
inline const char *Radio::errorString(Error e)
{
static constexpr const char *names[] = {
"ok", "not initialised", "spi open failed", "spi io failed",
"gpio open failed", "busy line never released", "chip is not LR1121",
"tx timeout", "tx low-battery detect", "rx timeout", "rx crc error",
};
const size_t i = size_t(e);
return i < (sizeof(names) / sizeof(*names)) ? names[i] : "unknown";
}
// ---------- Lifecycle ----------
inline bool Radio::beginRaw(const Config &cfg)
{
end();
cfg_ = cfg;
last_err_ = Error::Ok;
if (!openSpi()) return false;
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)) { last_err_ = Error::BusyTimeout; end(); return false; }
if (!waitBusy(500)) { end(); return false; }
return true;
}
inline bool Radio::begin(const Config &cfg)
{
if (!beginRaw(cfg)) return false;
const ChipVersion v = chipVersion();
if (v.type != 0x03) { last_err_ = Error::BadChip; end(); 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; }
if (!wcmd(OC_SET_FALLBACK_MODE, {0x01})) { 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; }
if (!wcmd(OC_CALIBRATE, {CALIB_ALL})) { end(); return false; }
(void)wcmd(OC_CLEAR_ERRORS);
{ const uint8_t p[] = {CALIB_ALL}; if (!wcmd(OC_CALIBRATE, p, sizeof(p))) { end(); return false; } }
(void)wcmd(OC_CLEAR_ERRORS, nullptr, 0);
if (!wcmd(OC_SET_REGMODE, {uint8_t(cfg_.use_dcdc)})) { end(); return false; }
if (!wcmd(OC_SET_PKT_TYPE, {PKT_TYPE_LORA})) { end(); return false; }
if (!setFrequency(cfg_.freq_hz)) { end(); return false; }
if (!wcmd(OC_SET_PKT_ADRS, {0x00, 0x00})) { end(); return false; }
if (!setModulation(cfg_.sf, cfg_.bw, cfg_.cr)) { end(); return false; }
if (!writePktParam(0xFF)) { end(); return false; }
if (!setTxPower(cfg_.tx_dbm, cfg_.pa_sel)) { end(); return false; }
if (!wcmd(OC_SET_LORA_NET, {uint8_t(cfg_.lora_wan)})) { end(); return false; }
// DIO5/DIO6 RF switch: STBY(0,0), RX(1,0), TX(0,1), TX_HP(0,1)
if (!wcmd(OC_SET_DIO_AS_RFSW, {0x03, 0, 0x01, 0x02, 0x02, 0, 0, 0})) {
end(); return false;
{ const uint8_t p[] = {static_cast<uint8_t>(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<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))) { 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<int>(cfg_.tx_dbm));
}
}
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.tx_dbm), TX_RAMP_48US}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) { end(); return false; } }
{ const uint8_t p[] = {static_cast<uint8_t>(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; }
return true;
}
inline bool Radio::setFrequency(uint32_t hz)
{
if (!wcmd(OC_SET_RF_FREQ, {
uint8_t(hz >> 24), uint8_t(hz >> 16),
uint8_t(hz >> 8), uint8_t(hz),
})) return false;
uint8_t f1, f2;
imgCalFreqs(hz, f1, f2);
if (!wcmd(OC_CALIBRATE_IMG, {f1, f2})) return false;
cfg_.freq_hz = hz;
return true;
}
inline bool Radio::setTxPower(int8_t dbm, uint8_t pa_sel)
{
if (!writePaCfg(pa_sel, dbm)) return false;
if (!wcmd(OC_SET_TX_PARAMS, {uint8_t(dbm), TX_RAMP_48US})) return false;
cfg_.pa_sel = pa_sel;
cfg_.tx_dbm = dbm;
return true;
}
inline bool Radio::setModulation(uint8_t sf, uint8_t bw, uint8_t cr)
{
if (!wcmd(OC_SET_MOD_PARAM, {sf, bw, cr, computeLdRo(sf, bw)})) return false;
cfg_.sf = sf;
cfg_.bw = bw;
cfg_.cr = cr;
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 fail(Error::NotReady);
if (spi_fd_ < 0 || busy_fd_ < 0 || reset_fd_ < 0 || dio9_fd_ < 0) return false;
if (!setStandbyRc()) return false;
(void)wcmd(OC_CLEAR_RXBUF);
(void)wcmd(OC_CLEAR_RXBUF, nullptr, 0);
(void)clearIrq();
return applyRadioSettings();
}
inline void Radio::reboot(bool stay_in_bootloader)
{
(void)wcmd(OC_REBOOT, {uint8_t(stay_in_bootloader ? 0x01 : 0x00)});
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));
}
@ -523,38 +479,39 @@ inline void Radio::end()
if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_fd_ = -1; }
}
// ---------- TX / RX ----------
inline bool Radio::send(const uint8_t *data, uint8_t n)
{
if (n == 0) return fail(Error::TxTimeout);
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;
const uint32_t mask = IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD;
if (!setIrqMask(mask, mask)) return false;
if (!setIrqMask(IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD, IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD)) return false;
if (!writePaCfg(pa_sel, tx_dbm)) return false;
if (!wcmd(OC_SET_TX_PARAMS, {uint8_t(tx_dbm), TX_RAMP_48US})) 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<uint8_t>(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;
if (!writePktParam(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);
if (!wcmd(OC_SET_TX, {
uint8_t(tx_steps >> 16),
uint8_t(tx_steps >> 8),
uint8_t(tx_steps),
})) return false;
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;
}
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(TX_TIMEOUT_MS + TX_POLL_GUARD_MS);
bool lbd_retry = 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 (readGpio(dio9_fd_) == 0) {
if (getGpioLine(dio9_fd_) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
@ -562,98 +519,76 @@ inline bool Radio::send(const uint8_t *data, uint8_t n)
if (irq & IRQ_TX_DONE) {
(void)clearIrq();
(void)setIrqMask(0, 0);
last_err_ = Error::Ok;
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 fail(Error::TxTimeout);
return false;
}
if (irq & IRQ_LBD) {
(void)clearIrq();
(void)setIrqMask(0, 0);
if (pa_sel == PA_HP) {
pa_sel = PA_LP;
if (pa_sel == 0x01) {
pa_sel = 0x00;
if (tx_dbm > 10) tx_dbm = 10;
} else if (tx_dbm > MIN_TX_DBM_FALLBACK) {
tx_dbm = int8_t(tx_dbm - 2);
tx_dbm = static_cast<int8_t>(tx_dbm - 2);
} else {
return fail(Error::TxLbd);
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX fail: LBD at minimum power (%d dBm)\n", static_cast<int>(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<int>(tx_dbm));
}
lbd_retry = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (!lbd_retry) {
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 fail(Error::TxTimeout);
return false;
}
}
return fail(Error::TxLbd);
return false;
}
inline bool Radio::startListening()
inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info)
{
if (!setStandbyXosc()) return false;
if (!clearIrq()) return false;
const uint32_t mask = IRQ_RX_DONE | IRQ_CRC_ERR;
if (!setIrqMask(mask, mask)) return false;
// 0xFFFFFF = continuous RX, no radio timeout
if (!wcmd(OC_SET_RX, {0xFF, 0xFF, 0xFF})) return false;
listening_ = true;
return true;
}
inline void Radio::stopListening()
{
(void)setStandbyXosc();
(void)clearIrq();
(void)setIrqMask(0, 0);
listening_ = false;
}
inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms,
RxInfo *rx_info)
{
if (!listening_) {
// one-shot: arm the chip, it will time out on its own
if (!setStandbyXosc()) return -1;
if (!clearIrq()) return -1;
const uint32_t mask = IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT;
if (!setIrqMask(mask, mask)) 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);
if (!wcmd(OC_SET_RX, {
uint8_t(steps >> 16),
uint8_t(steps >> 8),
uint8_t(steps),
})) return -1;
{
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;
}
// continuous: timeout_ms is a per-call poll window, 0 means wait forever
const uint32_t poll_ms = listening_ ? (timeout_ms == 0 ? 0 : timeout_ms)
: (timeout_ms == 0 ? 60000 : timeout_ms + 500);
const bool has_deadline = poll_ms > 0;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(poll_ms);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms == 0 ? 60000 : timeout_ms + 500);
for (;;) {
if (has_deadline && std::chrono::steady_clock::now() >= deadline) {
if (!listening_) { (void)clearIrq(); (void)setIrqMask(0, 0); }
last_err_ = Error::RxTimeout;
if (std::chrono::steady_clock::now() >= deadline) {
(void)clearIrq();
(void)setIrqMask(0, 0);
return -1;
}
if (readGpio(dio9_fd_) == 0) {
if (getGpioLine(dio9_fd_) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
const uint32_t irq = getIrq();
if (irq & IRQ_TIMEOUT) {
if (!listening_) { (void)clearIrq(); (void)setIrqMask(0, 0); }
last_err_ = Error::RxTimeout;
(void)clearIrq();
(void)setIrqMask(0, 0);
return -1;
}
if ((irq & (IRQ_RX_DONE | IRQ_CRC_ERR)) == 0) {
@ -664,30 +599,22 @@ inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms,
if (rx_info) {
uint8_t p[3]{};
if (rcmd(OC_GET_PKT_STATUS, nullptr, 0, p, sizeof(p))) {
rx_info->rssi_dbm = int8_t(-int(p[0]) / 2);
rx_info->snr_db = int8_t(int8_t(p[1]) / 4);
rx_info->signal_rssi_dbm = int8_t(-int(p[2]) / 2);
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 rp[] = {st[1], len};
if (len > 0 && !rcmd(OC_READ_BUF8, rp, sizeof(rp), buf, len)) 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);
(void)wcmd(OC_CLEAR_RXBUF, nullptr, 0);
(void)clearIrq();
if (listening_) {
// re-arm IRQ mask but leave chip in RX
const uint32_t mask = IRQ_RX_DONE | IRQ_CRC_ERR;
(void)setIrqMask(mask, mask);
} else {
(void)setIrqMask(0, 0);
}
if (irq & IRQ_CRC_ERR) { last_err_ = Error::RxCrc; return -2; }
last_err_ = Error::Ok;
return int(len);
return (irq & IRQ_CRC_ERR) ? -2 : static_cast<int>(len);
}
}

View File

@ -1,76 +0,0 @@
# lr1121_malnus.hpp — API reference
```cpp
#include "lr1121_malnus.hpp" // namespace lr1121
```
## Setup
```cpp
lr1121::Config cfg; // all fields have sensible defaults
cfg.freq_hz = lr1121::FREQ_433; // FREQ_433 / FREQ_868 / FREQ_2400
cfg.sf = 7; // spreading factor 612
cfg.bw = 0x04; // 0x03=62.5 kHz 0x04=125 kHz 0x05=250 kHz
cfg.cr = 0x01; // 0x01=4/5 0x02=4/6 0x03=4/7 0x04=4/8
cfg.tx_dbm = 10;
cfg.pa_sel = lr1121::PA_LP; // PA_LP (014 dBm) PA_HP (022 dBm)
lr1121::Radio radio;
if (!radio.begin(cfg))
fprintf(stderr, "init failed: %s\n", lr1121::Radio::errorString(radio.lastError()));
```
## TX / RX
```cpp
bool ok = radio.send(data, len); // blocks until TX_DONE or error
// returns: true=ok false=error (check lastError)
lr1121::RxInfo info;
// one-shot: arm, wait timeout_ms, chip returns to standby
int n = radio.receive(buf, sizeof(buf), 1000, &info);
// continuous: arm once, chip stays in RX between packets
radio.startListening();
for (;;) {
int n = radio.receive(buf, sizeof(buf), 0, &info); // 0 = wait forever per call
if (n > 0) { /* handle */ }
}
radio.stopListening(); // returns chip to standby
// receive() returns: n>=0 bytes -1=timeout/error -2=crc error
// info.rssi_dbm info.snr_db info.signal_rssi_dbm
```
## Runtime tuning *(call between packets, chip stays in standby)*
```cpp
radio.setFrequency(lr1121::FREQ_868);
radio.setTxPower(20, lr1121::PA_HP);
radio.setModulation(9, 0x04, 0x01); // sf, bw, cr
```
## Diagnostics
```cpp
lr1121::ChipVersion v = radio.chipVersion(); // v.hw v.type v.fw_hi v.fw_lo
float vbat = radio.vbatVolts();
uint16_t errs = radio.chipErrors(); // chip-side error flags
lr1121::Error e = radio.lastError();
const char *msg = lr1121::Radio::errorString(e);
```
## Errors
`Ok` `NotReady` `SpiOpen` `SpiIo` `GpioOpen` `BusyTimeout` `BadChip` `TxTimeout` `TxLbd` `RxTimeout` `RxCrc`
## Other
```cpp
radio.softResetSettings(); // re-apply Config without hard reset
radio.reboot(false); // reboot chip (true = stay in bootloader)
radio.end(); // close SPI + GPIO fds
const lr1121::Config &c = radio.config(); // current live config
```