Compare commits

...

3 Commits

Author SHA1 Message Date
shinya
b86c441045 updated readme and deploy script 2026-05-29 09:30:44 +02:00
shinya
d9b666e0f4 ai debloat 2026-05-29 09:29:57 +02:00
shinya
aaea30e762 update lora cleanup test 2026-05-29 09:29:41 +02:00
7 changed files with 582 additions and 406 deletions

View File

@ -8,9 +8,22 @@ 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. - `chip_test_example/` — LR1121 LoRa + ICM-20948 IMU + u-blox GPS drivers/tests.
- `main.cpp` — prototype that wires camera + shader + TCP together. - `main.cpp` — prototype that wires camera + shader + TCP together.
References: ## Goal
- https://www.youtube.com/watch?v=zFiubdrJqqI
- https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector/ 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.
## Build main.cpp ## Build main.cpp
@ -20,3 +33,8 @@ g++ main.cpp -o main \
-levent -levent_pthreads -lpthread \ -levent -levent_pthreads -lpthread \
-lturbojpeg -lglfw -lGLEW -lGL -lEGL -lGLESv2 -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: Run verbose first so you see exactly where it fails:
```sh ```sh
sudo ./lora_rx -v --433 sudo ./lora_rx -v
sudo ./imu_test -v sudo ./imu_test -v
./gps_test -v ./gps_test -v
``` ```
@ -72,7 +72,7 @@ sudo ./imu_test -v
```sh ```sh
ls /dev/spidev0.0 # SPI enabled? ls /dev/spidev0.0 # SPI enabled?
sudo ./lora_rx -v --433 # step labels show exactly which command hangs sudo ./lora_rx -v # step labels show exactly which command hangs
``` ```
If it hangs at `Calibrate` — the chip isn't responding over SPI at all. If it hangs at `Calibrate` — the chip isn't responding over SPI at all.
@ -81,13 +81,6 @@ 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 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. 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: If the chip is stuck in bootloader (fw < 0x02xx), escape with:
```sh ```sh

View File

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

View File

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

View File

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

View File

@ -1,21 +1,18 @@
// lr1121_malnus.hpp — minimal LR1121 LoRa driver for this board // lr1121_malnus.hpp — minimal LR1121 LoRa driver (Linux, header-only).
// //
// Board wiring: // Defaults: SPI /dev/spidev0.0, BUSY GPIO24, RESET GPIO25, DIO9 GPIO4.
// SPI : /dev/spidev0.0 (NSS GPIO8) // RF switch is driven by the chip via DIO5/DIO6.
// BUSY : GPIO24
// RESET: GPIO25
// RF switch controlled by LR1121 DIO5/DIO6
// //
// Command set references: // The driver never logs — failures return false (or a negative code); read
// - Semtech LR1121 user manual rev 1.2 // lastError()/errorString() for the reason. Callers decide what to print.
// - Semtech SWDR001 //
// - RadioLib LR11x0 behavior // Refs: Semtech LR1121 user manual rev 1.2, SWDR001, RadioLib LR11x0.
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstdio>
#include <cstring> #include <cstring>
#include <initializer_list>
#include <thread> #include <thread>
#include <fcntl.h> #include <fcntl.h>
@ -26,77 +23,70 @@
namespace lr1121 { namespace lr1121 {
constexpr uint16_t OC_GET_VERSION = 0x0101; // Opcodes (Semtech LR1121 user manual rev 1.2).
constexpr uint16_t OC_WRITE_BUF8 = 0x0109; constexpr uint16_t
constexpr uint16_t OC_READ_BUF8 = 0x010A; OC_GET_VERSION = 0x0101, OC_WRITE_BUF8 = 0x0109,
constexpr uint16_t OC_CLEAR_RXBUF = 0x010B; OC_READ_BUF8 = 0x010A, OC_CLEAR_RXBUF = 0x010B,
constexpr uint16_t OC_GET_ERRORS = 0x010D; OC_GET_ERRORS = 0x010D, OC_CLEAR_ERRORS = 0x010E,
constexpr uint16_t OC_CLEAR_ERRORS = 0x010E; OC_CALIBRATE = 0x010F, OC_SET_REGMODE = 0x0110,
constexpr uint16_t OC_CALIBRATE = 0x010F; OC_CALIBRATE_IMG = 0x0111, OC_SET_DIO_AS_RFSW = 0x0112,
constexpr uint16_t OC_SET_REGMODE = 0x0110; OC_SET_DIOIRQ = 0x0113, OC_CLEAR_IRQ = 0x0114,
constexpr uint16_t OC_CALIBRATE_IMAGE = 0x0111; OC_GET_IRQ = 0x0115, OC_REBOOT = 0x0118,
constexpr uint16_t OC_SET_DIO_AS_RFSW = 0x0112; OC_GET_VBAT = 0x0119, OC_SET_STANDBY = 0x011C,
constexpr uint16_t OC_SET_DIOIRQ = 0x0113; OC_GET_RXBUF_STA = 0x0203, OC_GET_PKT_STATUS = 0x0204,
constexpr uint16_t OC_CLEAR_IRQ = 0x0114; OC_SET_LORA_NET = 0x0208, OC_SET_RX = 0x0209,
constexpr uint16_t OC_GET_IRQ = 0x0115; OC_SET_TX = 0x020A, OC_SET_RF_FREQ = 0x020B,
constexpr uint16_t OC_REBOOT = 0x0118; OC_SET_PKT_TYPE = 0x020E, OC_SET_MOD_PARAM = 0x020F,
constexpr uint16_t OC_GET_VBAT = 0x0119; OC_SET_PKT_PARAM = 0x0210, OC_SET_TX_PARAMS = 0x0211,
constexpr uint16_t OC_SET_STANDBY = 0x011C; OC_SET_PKT_ADRS = 0x0212, OC_SET_FALLBACK_MODE = 0x0213,
OC_SET_PA_CFG = 0x0215;
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 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 IRQ_TX_DONE = (1u << 2); constexpr uint32_t FREQ_433 = 433'050'000u,
constexpr uint32_t IRQ_RX_DONE = (1u << 3); FREQ_868 = 868'000'000u,
constexpr uint32_t IRQ_CRC_ERR = (1u << 7); FREQ_2400 = 2'403'000'000u;
constexpr uint32_t IRQ_TIMEOUT = (1u << 10);
constexpr uint32_t IRQ_LBD = (1u << 21);
constexpr uint32_t IRQ_ALL = 0x1BF80FFCu;
constexpr uint32_t FREQ_433 = 433'050'000u; constexpr uint8_t PA_LP = 0x00, PA_HP = 0x01, PKT_TYPE_LORA = 0x02;
constexpr uint32_t FREQ_868 = 868'000'000u; constexpr uint8_t LORA_HEADER_EXPLICIT = 0x00, LORA_CRC_ON = 0x01, LORA_IQ_STD = 0x00;
constexpr uint32_t FREQ_2400 = 2'403'000'000u; constexpr uint16_t LORA_PREAMBLE_LEN = 8;
constexpr uint8_t LORA_PKT_HEADER_EXPLICIT = 0x00;
constexpr uint8_t LORA_PKT_PREAMBLE_LEN = 0x08;
constexpr uint8_t LORA_PKT_CRC_ON = 0x01;
constexpr uint8_t LORA_PKT_IQ_STD = 0x00;
constexpr uint8_t TX_RAMP_48US = 0x02; constexpr uint8_t TX_RAMP_48US = 0x02;
constexpr uint8_t PKT_TYPE_LORA = 0x02; constexpr uint32_t TX_TIMEOUT_MS = 3000, TX_POLL_GUARD_MS = 500;
constexpr uint32_t TX_TIMEOUT_MS = 3000;
constexpr uint32_t TX_POLL_GUARD_MS = 500;
constexpr int8_t MIN_TX_DBM_FALLBACK = 2; 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 { struct Config {
const char *spi_path = "/dev/spidev0.0"; const char *spi_path = "/dev/spidev0.0";
uint32_t spi_hz = 8'000'000; uint32_t spi_hz = 8'000'000;
const char *gpio_chip = "/dev/gpiochip0"; const char *gpio_chip = "/dev/gpiochip0";
unsigned busy_gpio = 24; unsigned busy_gpio = 24;
unsigned reset_gpio = 25; unsigned reset_gpio = 25;
unsigned dio9_gpio = 4; // IRQ line from LR1121 unsigned dio9_gpio = 4;
uint32_t freq_hz = FREQ_433; uint32_t freq_hz = FREQ_433;
uint8_t sf = 0x07; uint8_t sf = 7;
uint8_t bw = 0x04; uint8_t bw = 0x04; // 125 kHz
uint8_t cr = 0x01; uint8_t cr = 0x01; // 4/5
int8_t tx_dbm = 10; int8_t tx_dbm = 10;
uint8_t pa_sel = 0x00; // LP default to avoid LBD on weak VBAT rails uint8_t pa_sel = PA_LP; // LP avoids LBD on weak VBAT
uint8_t pa_supply = 0x00; uint8_t pa_supply = 0x00;
bool use_dcdc = false; bool use_dcdc = false;
bool lora_wan = false; bool lora_wan = false;
bool verbose = false;
}; };
struct RxInfo { struct RxInfo {
@ -106,10 +96,10 @@ struct RxInfo {
}; };
struct ChipVersion { struct ChipVersion {
uint8_t hw; uint8_t hw = 0;
uint8_t type; uint8_t type = 0;
uint8_t fw_hi; uint8_t fw_hi = 0;
uint8_t fw_lo; uint8_t fw_lo = 0;
}; };
class Radio { class Radio {
@ -119,44 +109,76 @@ public:
bool softResetSettings(); bool softResetSettings();
void end(); void end();
// Returns true on TX_DONE, false otherwise; check lastError() for reason.
bool send(const uint8_t *data, uint8_t n); 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(); // 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);
void reboot(bool stay_in_bootloader = false); void reboot(bool stay_in_bootloader = false);
private: private:
Config cfg_{}; Config cfg_{};
int spi_fd_ = -1; int spi_fd_ = -1, busy_fd_ = -1, reset_fd_ = -1, dio9_fd_ = -1;
int busy_fd_ = -1; bool listening_ = false;
int reset_fd_ = -1; Error last_err_ = Error::NotReady;
int dio9_fd_ = -1;
bool openSpi(const Config &cfg); bool fail(Error e) { last_err_ = e; return false; }
bool openSpi();
bool openGpio(unsigned line, bool out, int &fd_out); bool openGpio(unsigned line, bool out, int &fd_out);
int getGpioLine(int fd); int readGpio(int fd);
void setGpioLine(int fd, int value); void writeGpio(int fd, int value);
void hardReset(); void hardReset();
bool waitBusy(int timeout_ms = 1000); bool waitBusy(int timeout_ms = 1000);
bool spiTransfer(uint8_t *buf, size_t len); 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, 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 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 setStandbyXosc(); bool setStandbyXosc() { return wcmd(OC_SET_STANDBY, {0x01}); }
bool setStandbyRc(); bool setStandbyRc() { return wcmd(OC_SET_STANDBY, {0x00}); }
bool setIrqMask(uint32_t irq1, uint32_t irq2 = 0); bool setIrqMask(uint32_t irq1, uint32_t irq2 = 0);
bool clearIrq(uint32_t mask = IRQ_ALL); bool clearIrq(uint32_t mask = IRQ_ALL);
uint32_t getIrq(); uint32_t getIrq();
uint16_t getErrors();
bool applyRadioSettings(); 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 void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2);
static uint8_t computeLdRo(uint8_t sf, uint8_t bw); static uint8_t computeLdRo(uint8_t sf, uint8_t bw);
static uint32_t timeoutMsToRtcSteps(uint32_t ms); 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) inline bool Radio::spiTransfer(uint8_t *buf, size_t len)
{ {
spi_ioc_transfer tr{}; spi_ioc_transfer tr{};
@ -171,94 +193,74 @@ inline bool Radio::spiTransfer(uint8_t *buf, size_t len)
inline bool Radio::wcmd(uint16_t op, const uint8_t *params, size_t n) inline bool Radio::wcmd(uint16_t op, const uint8_t *params, size_t n)
{ {
if (n > 256) return false; if (n > 256) return false;
if (!waitBusy()) return false; if (!waitBusy()) return fail(Error::BusyTimeout);
uint8_t cmd[258]{}; uint8_t cmd[258]{};
cmd[0] = static_cast<uint8_t>(op >> 8); cmd[0] = uint8_t(op >> 8);
cmd[1] = static_cast<uint8_t>(op & 0xFF); cmd[1] = uint8_t(op);
if (params && n) std::memcpy(cmd + 2, params, n); if (params && n) std::memcpy(cmd + 2, params, n);
if (!spiTransfer(cmd, 2 + n)) return false; if (!spiTransfer(cmd, 2 + n)) return fail(Error::SpiIo);
return waitBusy(); if (!waitBusy()) return fail(Error::BusyTimeout);
return true;
} }
inline bool Radio::rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr) 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)
{ {
if (np > 30 || nr > 259) return false; if (np > 30 || nr > 259) return false;
if (!waitBusy()) return false; if (!waitBusy()) return fail(Error::BusyTimeout);
uint8_t cmd[32]{}; uint8_t cmd[32]{};
cmd[0] = static_cast<uint8_t>(op >> 8); cmd[0] = uint8_t(op >> 8);
cmd[1] = static_cast<uint8_t>(op & 0xFF); cmd[1] = uint8_t(op);
if (params && np) std::memcpy(cmd + 2, params, np); if (params && np) std::memcpy(cmd + 2, params, np);
if (!spiTransfer(cmd, 2 + np)) return false; if (!spiTransfer(cmd, 2 + np)) return fail(Error::SpiIo);
if (!waitBusy()) return fail(Error::BusyTimeout);
if (!waitBusy()) return false;
uint8_t rsp[260]{}; uint8_t rsp[260]{};
if (!spiTransfer(rsp, nr + 1)) return false; if (!spiTransfer(rsp, nr + 1)) return fail(Error::SpiIo);
std::memcpy(out, rsp + 1, nr); std::memcpy(out, rsp + 1, nr);
return true; return true;
} }
inline bool Radio::setIrqMask(uint32_t irq1, uint32_t irq2) inline bool Radio::setIrqMask(uint32_t irq1, uint32_t irq2)
{ {
const uint8_t p[8] = { return wcmd(OC_SET_DIOIRQ, {
static_cast<uint8_t>(irq1 >> 24), static_cast<uint8_t>(irq1 >> 16), uint8_t(irq1 >> 24), uint8_t(irq1 >> 16), uint8_t(irq1 >> 8), uint8_t(irq1),
static_cast<uint8_t>(irq1 >> 8), static_cast<uint8_t>(irq1), uint8_t(irq2 >> 24), uint8_t(irq2 >> 16), uint8_t(irq2 >> 8), uint8_t(irq2),
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) inline bool Radio::clearIrq(uint32_t mask)
{ {
const uint8_t p[4] = { return wcmd(OC_CLEAR_IRQ, {
static_cast<uint8_t>(mask >> 24), static_cast<uint8_t>(mask >> 16), uint8_t(mask >> 24), uint8_t(mask >> 16), uint8_t(mask >> 8), uint8_t(mask),
static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask), });
};
return wcmd(OC_CLEAR_IRQ, p, sizeof(p));
} }
inline uint32_t Radio::getIrq() inline uint32_t Radio::getIrq()
{ {
uint8_t b[4]{}; uint8_t b[4]{};
if (!rcmd(OC_GET_IRQ, nullptr, 0, b, sizeof(b))) return 0; if (!rcmd(OC_GET_IRQ, nullptr, 0, b, sizeof(b))) return 0;
return (static_cast<uint32_t>(b[0]) << 24) | return (uint32_t(b[0]) << 24) | (uint32_t(b[1]) << 16) |
(static_cast<uint32_t>(b[1]) << 16) | (uint32_t(b[2]) << 8) | uint32_t(b[3]);
(static_cast<uint32_t>(b[2]) << 8) |
static_cast<uint32_t>(b[3]);
} }
inline uint16_t Radio::getErrors() // ---------- SPI / GPIO open ----------
{
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]);
}
inline bool Radio::setStandbyRc() inline bool Radio::openSpi()
{ {
const uint8_t p[] = {0x00}; spi_fd_ = ::open(cfg_.spi_path, O_RDWR);
return wcmd(OC_SET_STANDBY, p, sizeof(p)); if (spi_fd_ < 0) return fail(Error::SpiOpen);
}
inline bool Radio::setStandbyXosc() uint8_t mode = SPI_MODE_0, bits = 8;
{
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 || 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_BITS_PER_WORD, &bits) < 0 ||
ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg.spi_hz) < 0) { ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg_.spi_hz) < 0) {
::close(spi_fd_); ::close(spi_fd_); spi_fd_ = -1;
spi_fd_ = -1; return fail(Error::SpiOpen);
return false;
} }
return true; return true;
} }
@ -266,7 +268,7 @@ inline bool Radio::openSpi(const Config &cfg)
inline bool Radio::openGpio(unsigned line, bool out, int &fd_out) inline bool Radio::openGpio(unsigned line, bool out, int &fd_out)
{ {
int chip = ::open(cfg_.gpio_chip, O_RDWR); int chip = ::open(cfg_.gpio_chip, O_RDWR);
if (chip < 0) return false; if (chip < 0) return fail(Error::GpioOpen);
gpiohandle_request req{}; gpiohandle_request req{};
req.lineoffsets[0] = line; req.lineoffsets[0] = line;
@ -274,22 +276,22 @@ inline bool Radio::openGpio(unsigned line, bool out, int &fd_out)
req.flags = out ? GPIOHANDLE_REQUEST_OUTPUT : GPIOHANDLE_REQUEST_INPUT; req.flags = out ? GPIOHANDLE_REQUEST_OUTPUT : GPIOHANDLE_REQUEST_INPUT;
req.default_values[0] = out ? 1 : 0; req.default_values[0] = out ? 1 : 0;
std::strncpy(req.consumer_label, "lr1121", sizeof(req.consumer_label) - 1); std::strncpy(req.consumer_label, "lr1121", sizeof(req.consumer_label) - 1);
const int rc = ioctl(chip, GPIO_GET_LINEHANDLE_IOCTL, &req); const int rc = ioctl(chip, GPIO_GET_LINEHANDLE_IOCTL, &req);
::close(chip); ::close(chip);
if (rc < 0) return false; if (rc < 0) return fail(Error::GpioOpen);
fd_out = req.fd; fd_out = req.fd;
return true; return true;
} }
inline int Radio::getGpioLine(int fd) inline int Radio::readGpio(int fd)
{ {
gpiohandle_data d{}; gpiohandle_data d{};
if (ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &d) < 0) return 1; if (ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &d) < 0) return 1;
return d.values[0]; return d.values[0];
} }
inline void Radio::setGpioLine(int fd, int value) inline void Radio::writeGpio(int fd, int value)
{ {
gpiohandle_data d{}; gpiohandle_data d{};
d.values[0] = value; d.values[0] = value;
@ -298,9 +300,9 @@ inline void Radio::setGpioLine(int fd, int value)
inline void Radio::hardReset() inline void Radio::hardReset()
{ {
setGpioLine(reset_fd_, 0); writeGpio(reset_fd_, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
setGpioLine(reset_fd_, 1); writeGpio(reset_fd_, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }
@ -308,49 +310,46 @@ inline bool Radio::waitBusy(int timeout_ms)
{ {
const int loops = timeout_ms * 20; const int loops = timeout_ms * 20;
for (int i = 0; i < loops; ++i) { for (int i = 0; i < loops; ++i) {
if (getGpioLine(busy_fd_) == 0) return true; if (readGpio(busy_fd_) == 0) return true;
std::this_thread::sleep_for(std::chrono::microseconds(50)); std::this_thread::sleep_for(std::chrono::microseconds(50));
} }
return false; return false;
} }
// ---------- Math helpers ----------
inline void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2) inline void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2)
{ {
const uint32_t mhz = hz / 1'000'000u; const uint32_t mhz = hz / 1'000'000u;
uint32_t lo = 430, hi = 440; uint32_t lo, hi;
if (mhz < 446) { // 2.4 GHz values are hardcoded (lo/4 would overflow uint8_t).
lo = 430; hi = 440; if (mhz >= 2000) { f1 = 0xD7; f2 = 0xDB; return; }
} else if (mhz < 740) { if (mhz < 446) { lo = 430; hi = 440; }
lo = 470; hi = 510; else if (mhz < 740) { lo = 470; hi = 510; }
} else if (mhz < 890) { else if (mhz < 890) { lo = 860; hi = 876; }
lo = 860; hi = 876; else { lo = 902; hi = 928; }
} else if (mhz < 2000) { f1 = uint8_t(lo / 4);
lo = 902; hi = 928; f2 = uint8_t((hi + 3) / 4);
} 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)
{ {
static const uint32_t bw_hz[] = {0, 15625, 31250, 62500, 125000, 250000, 500000}; 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 bw_val = (bw < 7) ? bw_hz[bw] : 125000;
const uint32_t sym_ms = (1000u << sf) / bw_val; return ((1000u << sf) / bw_val) > 16 ? 1 : 0;
return (sym_ms > 16) ? 1 : 0;
} }
inline uint32_t Radio::timeoutMsToRtcSteps(uint32_t ms) inline uint32_t Radio::timeoutMsToRtcSteps(uint32_t ms)
{ {
if (ms == 0) return 0x00FFFFFFu; if (ms == 0) return 0x00FFFFFFu;
const uint64_t steps = (static_cast<uint64_t>(ms) * 32768u) / 1000u; const uint64_t steps = (uint64_t(ms) * 32768u) / 1000u;
return (steps > 0x00FFFFFFu) ? 0x00FFFFFFu : static_cast<uint32_t>(steps); return steps > 0x00FFFFFFu ? 0x00FFFFFFu : 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 == 0x01) { if (pa_sel == PA_HP) {
if (dbm >= 22) { duty = 4; hp_max = 7; } if (dbm >= 22) { duty = 4; hp_max = 7; }
else if (dbm >= 20) { duty = 3; hp_max = 5; } else if (dbm >= 20) { duty = 3; hp_max = 5; }
else if (dbm >= 17) { duty = 2; hp_max = 3; } else if (dbm >= 17) { duty = 2; hp_max = 3; }
@ -366,108 +365,153 @@ inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, ui
} }
} }
inline ChipVersion Radio::getVersion() 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()
{ {
uint8_t v[4]{}; uint8_t v[4]{};
(void)rcmd(OC_GET_VERSION, nullptr, 0, v, sizeof(v)); (void)rcmd(OC_GET_VERSION, nullptr, 0, v, sizeof(v));
return {v[0], v[1], v[2], v[3]}; 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) inline bool Radio::beginRaw(const Config &cfg)
{ {
end(); end();
cfg_ = cfg; cfg_ = cfg;
if (!openSpi(cfg_)) return false; last_err_ = Error::Ok;
if (!openSpi()) return false;
if (!openGpio(cfg_.reset_gpio, true, reset_fd_)) { end(); 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_.busy_gpio, false, busy_fd_)) { end(); return false; }
if (!openGpio(cfg_.dio9_gpio, false, dio9_fd_)) { end(); return false; } if (!openGpio(cfg_.dio9_gpio, false, dio9_fd_)) { end(); return false; }
hardReset(); hardReset();
if (!waitBusy(500)) { end(); return false; } if (!waitBusy(500)) { last_err_ = Error::BusyTimeout; end(); return false; }
return true; return true;
} }
inline bool Radio::begin(const Config &cfg) inline bool Radio::begin(const Config &cfg)
{ {
if (!beginRaw(cfg)) return false; if (!beginRaw(cfg)) return false;
const ChipVersion v = chipVersion();
const ChipVersion v = getVersion(); if (v.type != 0x03) { last_err_ = Error::BadChip; end(); return false; }
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(); return applyRadioSettings();
} }
inline bool Radio::applyRadioSettings() inline bool Radio::applyRadioSettings()
{ {
if (!setStandbyRc()) { end(); return false; } if (!setStandbyRc()) { end(); return false; }
{ const uint8_t p[] = {0x01}; if (!wcmd(OC_SET_FALLBACK_MODE, p, sizeof(p))) { end(); return false; } } if (!wcmd(OC_SET_FALLBACK_MODE, {0x01})) { end(); return false; }
if (!clearIrq()) { end(); return false; } if (!clearIrq()) { end(); return false; }
if (!setIrqMask(0, 0)) { end(); return false; } if (!setIrqMask(0, 0)) { end(); return false; }
{ const uint8_t p[] = {CALIB_ALL}; if (!wcmd(OC_CALIBRATE, p, sizeof(p))) { end(); return false; } } if (!wcmd(OC_CALIBRATE, {CALIB_ALL})) { end(); return false; }
(void)wcmd(OC_CLEAR_ERRORS, nullptr, 0); (void)wcmd(OC_CLEAR_ERRORS);
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.use_dcdc ? 0x01 : 0x00)}; if (!wcmd(OC_SET_REGMODE, p, sizeof(p))) { end(); return false; } } if (!wcmd(OC_SET_REGMODE, {uint8_t(cfg_.use_dcdc)})) { end(); return false; }
{ const uint8_t p[] = {PKT_TYPE_LORA}; if (!wcmd(OC_SET_PKT_TYPE, p, sizeof(p))) { end(); return false; } } if (!wcmd(OC_SET_PKT_TYPE, {PKT_TYPE_LORA})) { end(); return false; }
{ if (!setFrequency(cfg_.freq_hz)) { end(); return false; }
const uint8_t p[] = { if (!wcmd(OC_SET_PKT_ADRS, {0x00, 0x00})) { end(); return false; }
static_cast<uint8_t>(cfg_.freq_hz >> 24), static_cast<uint8_t>(cfg_.freq_hz >> 16), if (!setModulation(cfg_.sf, cfg_.bw, cfg_.cr)) { end(); return false; }
static_cast<uint8_t>(cfg_.freq_hz >> 8), static_cast<uint8_t>(cfg_.freq_hz), if (!writePktParam(0xFF)) { end(); return false; }
}; if (!setTxPower(cfg_.tx_dbm, cfg_.pa_sel)) { end(); return false; }
if (!wcmd(OC_SET_RF_FREQ, p, sizeof(p))) { end(); return false; } if (!wcmd(OC_SET_LORA_NET, {uint8_t(cfg_.lora_wan)})) { 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; } }
{ // DIO5/DIO6 RF switch: STBY(0,0), RX(1,0), TX(0,1), TX_HP(0,1)
uint8_t duty = 0, hp_max = 0; if (!wcmd(OC_SET_DIO_AS_RFSW, {0x03, 0, 0x01, 0x02, 0x02, 0, 0, 0})) {
computePaConfig(cfg_.pa_sel, cfg_.tx_dbm, duty, hp_max); end(); return false;
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; } if (!setStandbyXosc()) { end(); return false; }
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] init OK: %u Hz SF%u BW=0x%02X CR=0x%02X\n", cfg_.freq_hz, cfg_.sf, cfg_.bw, cfg_.cr); return true;
}
inline bool Radio::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;
return true; return true;
} }
inline bool Radio::softResetSettings() inline bool Radio::softResetSettings()
{ {
if (spi_fd_ < 0 || busy_fd_ < 0 || reset_fd_ < 0 || dio9_fd_ < 0) return false; if (spi_fd_ < 0 || busy_fd_ < 0 || reset_fd_ < 0 || dio9_fd_ < 0)
return fail(Error::NotReady);
if (!setStandbyRc()) return false; if (!setStandbyRc()) return false;
(void)wcmd(OC_CLEAR_RXBUF, nullptr, 0); (void)wcmd(OC_CLEAR_RXBUF);
(void)clearIrq(); (void)clearIrq();
return applyRadioSettings(); return applyRadioSettings();
} }
inline void Radio::reboot(bool stay_in_bootloader) inline void Radio::reboot(bool stay_in_bootloader)
{ {
const uint8_t p[] = { static_cast<uint8_t>(stay_in_bootloader ? 0x01 : 0x00) }; (void)wcmd(OC_REBOOT, {uint8_t(stay_in_bootloader ? 0x01 : 0x00)});
(void)wcmd(OC_REBOOT, p, sizeof(p));
std::this_thread::sleep_for(std::chrono::milliseconds(300)); std::this_thread::sleep_for(std::chrono::milliseconds(300));
} }
@ -479,39 +523,38 @@ inline void Radio::end()
if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_fd_ = -1; } if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_fd_ = -1; }
} }
// ---------- TX / RX ----------
inline bool Radio::send(const uint8_t *data, uint8_t n) inline bool Radio::send(const uint8_t *data, uint8_t n)
{ {
if (n == 0) return false; if (n == 0) return fail(Error::TxTimeout);
uint8_t pa_sel = cfg_.pa_sel; uint8_t pa_sel = cfg_.pa_sel;
int8_t tx_dbm = cfg_.tx_dbm; int8_t tx_dbm = cfg_.tx_dbm;
for (int attempt = 0; attempt < 6; ++attempt) { for (int attempt = 0; attempt < 6; ++attempt) {
if (!setStandbyXosc()) return false; if (!setStandbyXosc()) return false;
if (!clearIrq()) return false; if (!clearIrq()) return false;
if (!setIrqMask(IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD, IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD)) return false; const uint32_t mask = IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD;
if (!setIrqMask(mask, mask)) return false;
{ if (!writePaCfg(pa_sel, tx_dbm)) return false;
uint8_t duty = 0, hp_max = 0; if (!wcmd(OC_SET_TX_PARAMS, {uint8_t(tx_dbm), TX_RAMP_48US})) return false;
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 (!wcmd(OC_WRITE_BUF8, data, n)) return false;
{ const uint8_t p[] = {LORA_PKT_HEADER_EXPLICIT, LORA_PKT_PREAMBLE_LEN, 0x00, n, LORA_PKT_CRC_ON, LORA_PKT_IQ_STD}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; } if (!writePktParam(n)) return false;
{
const uint32_t tx_steps = timeoutMsToRtcSteps(TX_TIMEOUT_MS);
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); 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 auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(TX_TIMEOUT_MS + TX_POLL_GUARD_MS);
bool lbd_retry = false;
while (std::chrono::steady_clock::now() < deadline) { while (std::chrono::steady_clock::now() < deadline) {
if (getGpioLine(dio9_fd_) == 0) { if (readGpio(dio9_fd_) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue; continue;
} }
@ -519,76 +562,98 @@ inline bool Radio::send(const uint8_t *data, uint8_t n)
if (irq & IRQ_TX_DONE) { if (irq & IRQ_TX_DONE) {
(void)clearIrq(); (void)clearIrq();
(void)setIrqMask(0, 0); (void)setIrqMask(0, 0);
last_err_ = Error::Ok;
return true; return true;
} }
if (irq & IRQ_TIMEOUT) { 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)clearIrq();
(void)setIrqMask(0, 0); (void)setIrqMask(0, 0);
return false; return fail(Error::TxTimeout);
} }
if (irq & IRQ_LBD) { if (irq & IRQ_LBD) {
(void)clearIrq(); (void)clearIrq();
(void)setIrqMask(0, 0); (void)setIrqMask(0, 0);
if (pa_sel == 0x01) { if (pa_sel == PA_HP) {
pa_sel = 0x00; pa_sel = PA_LP;
if (tx_dbm > 10) tx_dbm = 10; if (tx_dbm > 10) tx_dbm = 10;
} else if (tx_dbm > MIN_TX_DBM_FALLBACK) { } else if (tx_dbm > MIN_TX_DBM_FALLBACK) {
tx_dbm = static_cast<int8_t>(tx_dbm - 2); tx_dbm = int8_t(tx_dbm - 2);
} else { } else {
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX fail: LBD at minimum power (%d dBm)\n", static_cast<int>(tx_dbm)); return fail(Error::TxLbd);
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; break;
} }
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
} }
if (std::chrono::steady_clock::now() >= deadline) { if (!lbd_retry) {
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX poll timeout errs=0x%04X\n", getErrors());
(void)clearIrq(); (void)clearIrq();
(void)setIrqMask(0, 0); (void)setIrqMask(0, 0);
return false; return fail(Error::TxTimeout);
} }
} }
return false; return fail(Error::TxLbd);
} }
inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info) inline bool Radio::startListening()
{ {
if (!setStandbyXosc()) return -1; if (!setStandbyXosc()) return false;
if (!clearIrq()) return -1; if (!clearIrq()) return false;
if (!setIrqMask(IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT, IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT)) return -1; 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;
}
const uint32_t steps = timeoutMsToRtcSteps(timeout_ms); inline void Radio::stopListening()
{ {
const uint8_t p[] = { (void)setStandbyXosc();
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 (;;) {
if (std::chrono::steady_clock::now() >= deadline) {
(void)clearIrq(); (void)clearIrq();
(void)setIrqMask(0, 0); (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;
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;
}
// 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);
for (;;) {
if (has_deadline && std::chrono::steady_clock::now() >= deadline) {
if (!listening_) { (void)clearIrq(); (void)setIrqMask(0, 0); }
last_err_ = Error::RxTimeout;
return -1; return -1;
} }
if (getGpioLine(dio9_fd_) == 0) { if (readGpio(dio9_fd_) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue; continue;
} }
const uint32_t irq = getIrq(); const uint32_t irq = getIrq();
if (irq & IRQ_TIMEOUT) { if (irq & IRQ_TIMEOUT) {
(void)clearIrq(); if (!listening_) { (void)clearIrq(); (void)setIrqMask(0, 0); }
(void)setIrqMask(0, 0); last_err_ = Error::RxTimeout;
return -1; return -1;
} }
if ((irq & (IRQ_RX_DONE | IRQ_CRC_ERR)) == 0) { if ((irq & (IRQ_RX_DONE | IRQ_CRC_ERR)) == 0) {
@ -599,22 +664,30 @@ inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo
if (rx_info) { if (rx_info) {
uint8_t p[3]{}; uint8_t p[3]{};
if (rcmd(OC_GET_PKT_STATUS, nullptr, 0, p, sizeof(p))) { 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->rssi_dbm = int8_t(-int(p[0]) / 2);
rx_info->snr_db = static_cast<int8_t>(static_cast<int8_t>(p[1]) / 4); rx_info->snr_db = int8_t(int8_t(p[1]) / 4);
rx_info->signal_rssi_dbm = static_cast<int8_t>(-(int)p[2] / 2); rx_info->signal_rssi_dbm = int8_t(-int(p[2]) / 2);
} }
} }
uint8_t st[2]{}; uint8_t st[2]{};
if (!rcmd(OC_GET_RXBUF_STA, nullptr, 0, st, sizeof(st))) return -1; 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 len = st[0] < cap ? st[0] : cap;
const uint8_t p[] = {st[1], len}; const uint8_t rp[] = {st[1], len};
if (len > 0 && !rcmd(OC_READ_BUF8, p, sizeof(p), buf, len)) return -1; if (len > 0 && !rcmd(OC_READ_BUF8, rp, sizeof(rp), buf, len)) return -1;
(void)wcmd(OC_CLEAR_RXBUF, nullptr, 0); (void)wcmd(OC_CLEAR_RXBUF);
(void)clearIrq(); (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); (void)setIrqMask(0, 0);
return (irq & IRQ_CRC_ERR) ? -2 : static_cast<int>(len); }
if (irq & IRQ_CRC_ERR) { last_err_ = Error::RxCrc; return -2; }
last_err_ = Error::Ok;
return int(len);
} }
} }

View File

@ -0,0 +1,76 @@
# 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
```