Compare commits

..

2 Commits

Author SHA1 Message Date
shinya
7f1aabb280 ai code icm and lora, fixes needed 2026-05-21 15:27:45 +02:00
shinya
a88ce8c12f cleanup and fixes 2026-05-21 15:23:13 +02:00
9 changed files with 1111 additions and 39 deletions

View File

@ -168,6 +168,7 @@ void processRequest(Request *request)
int main()
{
std::signal(SIGPIPE, SIG_IGN);
std::signal(SIGINT, [](int) { loop.exit(0); });
std::thread(serverThread).detach();
auto cm = std::make_unique<CameraManager>();

View File

@ -0,0 +1,28 @@
# Build on device:
# apt install g++
# make
# All deps (spidev, gpiochip, i2c-dev, termios) are kernel headers — no apt libs needed.
CXX ?= g++
CXXFLAGS ?= -O2 -std=c++17 -Wall -Wextra
TARGETS = lora_tx lora_rx imu_test gps_test
all: $(TARGETS)
lora_tx: lora_tx.cpp lr1121_malnus.hpp
$(CXX) $(CXXFLAGS) $< -lpthread -o $@
lora_rx: lora_rx.cpp lr1121_malnus.hpp
$(CXX) $(CXXFLAGS) $< -lpthread -o $@
imu_test: imu_test.cpp icm20948.hpp
$(CXX) $(CXXFLAGS) $< -lpthread -o $@
gps_test: gps_test.cpp gps.hpp
$(CXX) $(CXXFLAGS) $< -o $@
clean:
rm -f $(TARGETS)
.PHONY: all clean

View File

@ -3,24 +3,32 @@
Tests for LR1121 LoRa, ICM-20948 IMU, u-blox GPS on Raspberry Pi Zero W 2.
Header-only drivers, kernel ioctls, no external libs.
Driver: `lr1121_malnus.hpp` — crystal oscillator, RF switch via DIO5/DIO6.
---
## Wiring
### LR1121 (SPI0)
| Module | Pi GPIO | Pi pin |
|--------|---------|--------|
| SCK | GPIO11 | 23 |
| MOSI | GPIO10 | 19 |
| MISO | GPIO9 | 21 |
| NSS | GPIO8 | 24 |
| BUSY | GPIO24 | 18 |
| NRESET | GPIO25 | 22 |
| DIO9 | GPIO4 | 7 |
| DIO8 | GPIO23 | 16 |
| Module | Pi GPIO | Pi pin |
|---------|---------|--------|
| SCK | GPIO11 | 23 |
| MOSI | GPIO10 | 19 |
| MISO | GPIO9 | 21 |
| NSS | GPIO8 | 24 |
| BUSY | GPIO24 | 18 |
| NRESET | GPIO25 | 22 |
| DIO5 | — | chip-driven RF switch (RFSW0) |
| DIO6 | — | chip-driven RF switch (RFSW1) |
| DIO9 | GPIO4 | 7 |
| DIO8 | GPIO23 | 16 |
Enable: `sudo raspi-config` → Interfaces → SPI → Yes → reboot
DIO5 and DIO6 are driven directly by the LR1121 — they go to your module's RF
switch and do not connect to the Pi. The driver configures them automatically
via `SetDioAsRfSwitch`: HIGH on DIO5 in RX, HIGH on DIO6 in TX, both LOW in standby.
Enable SPI: `sudo raspi-config` → Interfaces → SPI → Yes → reboot
### ICM-20948 (I2C1)
@ -63,28 +71,29 @@ sudo ./imu_test -v
## LoRa debug
```sh
ls /dev/spidev0.0 # SPI on?
ls /dev/spidev0.0 # SPI enabled?
sudo ./lora_rx -v --433 # step labels show exactly which command hangs
```
If it hangs at `Calibrate` — that's a TCXO config issue. Try in order:
If it hangs at `Calibrate` — the chip isn't responding over SPI at all.
Check wiring, CS, and that SPI is enabled. The crystal needs no tuning.
```sh
sudo ./lora_rx -v --433 --tcxo-none # skip TCXO entirely (crystal mode)
sudo ./lora_rx -v --433 --tcxo-27 # TCXO 2.7V
sudo ./lora_rx -v --433 --tcxo-33 # TCXO 3.3V (default)
```
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.
The one that gets past "Calibrate done" is your module's config.
Use the same TCXO flag on TX and RX.
If using the 2.4 GHz antenna instead:
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
sudo ./lora_rx --reset
```
---
## IMU debug
@ -107,14 +116,3 @@ stty -F /dev/serial0 38400 raw && cat /dev/serial0 # raw NMEA bytes
./gps_test -v -a # all sentences
./gps_test -b 9600 # try different baud
```
---
## TCXO voltage reference
| Flag | Value | Voltage |
|------|-------|---------|
| `--tcxo-none` | 0xFF | no TCXO (crystal) |
| `--tcxo-27` | 0x05 | 2.7V |
| `--tcxo-33` | 0x07 | 3.3V |
| `--tcxo-v N` | 0x000x07 | raw byte |

View File

@ -0,0 +1,266 @@
// icm20948.hpp — InvenSense ICM-20948 IMU driver
// Linux /dev/i2c-1 via I2C_RDWR ioctl — no external libs, no libgpiod.
//
// Hardware (THE TRUTH):
// SDA = GPIO2 (pin 3), SCL = GPIO3 (pin 5)
// VDD = 3.3V, GND = GND
// AD0 pin determines I2C address: GND → 0x68, VCC → 0x69
//
// If i2cdetect -y 1 shows nothing:
// 1. Enable I2C: sudo raspi-config → Interfaces → I2C → Yes → reboot
// 2. Check wiring — SDA/SCL must have pull-ups (Pi has built-in 1.8kΩ)
// 3. Verify AD0 pin state to confirm address (0x68 vs 0x69)
// 4. Try slower I2C: add i2c_arm_baudrate=50000 in /boot/config.txt
#pragma once
#include <chrono>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <thread>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>
#include <unistd.h>
namespace icm20948 {
// ---- Register map (Bank 0) -------------------------------------------------
constexpr uint8_t REG_WHO_AM_I = 0x00; // should read 0xEA
constexpr uint8_t REG_USER_CTRL = 0x03;
constexpr uint8_t REG_PWR_MGMT_1 = 0x06; // bit7=DEVICE_RESET, bits2:0=CLKSEL
constexpr uint8_t REG_PWR_MGMT_2 = 0x07; // bit5:3=disable_accel, bit2:0=disable_gyro
constexpr uint8_t REG_ACCEL_XOUT_H = 0x2D;
constexpr uint8_t REG_GYRO_XOUT_H = 0x33;
constexpr uint8_t REG_TEMP_OUT_H = 0x39;
constexpr uint8_t REG_INT_STATUS = 0x19;
constexpr uint8_t REG_BANK_SEL = 0x7F;
// ---- Register map (Bank 2) -------------------------------------------------
constexpr uint8_t B2_GYRO_SMPLRT_DIV = 0x00;
constexpr uint8_t B2_GYRO_CONFIG_1 = 0x01; // bits3:2=FS_SEL bits1:0=DLPF
constexpr uint8_t B2_ACCEL_SMPLRT_DIV_1 = 0x10;
constexpr uint8_t B2_ACCEL_SMPLRT_DIV_2 = 0x11;
constexpr uint8_t B2_ACCEL_CONFIG = 0x14; // bits3:2=FS_SEL bits1:0=DLPF
constexpr uint8_t WHO_AM_I_VAL = 0xEA;
// ---- Scale configuration ---------------------------------------------------
// Accel full-scale: 0=±2g 1=±4g 2=±8g 3=±16g
// Gyro full-scale: 0=±250 1=±500 2=±1000 3=±2000 dps
constexpr float ACCEL_SCALE[4] = { 2.0f/32768, 4.0f/32768, 8.0f/32768, 16.0f/32768 };
constexpr float GYRO_SCALE[4] = { 250.0f/32768, 500.0f/32768, 1000.0f/32768, 2000.0f/32768 };
// Raw 16-bit sensor sample
struct RawSample {
int16_t ax, ay, az; // accelerometer
int16_t gx, gy, gz; // gyroscope
int16_t temp_raw; // temperature raw
};
// Scaled sample with physical units
struct Sample {
float ax, ay, az; // g
float gx, gy, gz; // deg/s
float temp_c; // Celsius
};
struct Config {
const char *i2c_path = "/dev/i2c-1";
uint8_t accel_fs = 0; // 0=±2g, 1=±4g, 2=±8g, 3=±16g
uint8_t gyro_fs = 0; // 0=±250dps, 1=±500, 2=±1000, 3=±2000
bool verbose = false;
};
class Imu {
public:
bool begin(const Config &cfg);
void end();
// Returns true when new data is read.
bool read(RawSample &raw);
bool read(Sample &s);
// Returns the detected I2C address (0x68 or 0x69), or 0 if not found.
uint8_t address() const { return addr_; }
// Direct register access (for debugging / bank switching experiments).
bool writeReg(uint8_t reg, uint8_t val);
uint8_t readReg(uint8_t reg);
bool readRegs(uint8_t reg, uint8_t *buf, uint8_t n);
bool selectBank(uint8_t bank); // 03
private:
Config cfg_{};
int fd_ = -1;
uint8_t addr_ = 0;
uint8_t accel_fs_ = 0;
uint8_t gyro_fs_ = 0;
};
// ---- Implementation ---------------------------------------------------------
inline bool Imu::writeReg(uint8_t reg, uint8_t val)
{
uint8_t buf[2] = { reg, val };
i2c_msg msg{};
msg.addr = addr_;
msg.flags = 0;
msg.len = 2;
msg.buf = buf;
i2c_rdwr_ioctl_data io{};
io.msgs = &msg;
io.nmsgs = 1;
return ioctl(fd_, I2C_RDWR, &io) == 1;
}
inline bool Imu::readRegs(uint8_t reg, uint8_t *buf, uint8_t n)
{
// Generates proper repeated-start: [START|ADDR+W|REG|RSTART|ADDR+R|DATA|STOP]
i2c_msg msgs[2]{};
msgs[0].addr = addr_;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &reg;
msgs[1].addr = addr_;
msgs[1].flags = I2C_M_RD;
msgs[1].len = n;
msgs[1].buf = buf;
i2c_rdwr_ioctl_data io{};
io.msgs = msgs;
io.nmsgs = 2;
return ioctl(fd_, I2C_RDWR, &io) == 2;
}
inline uint8_t Imu::readReg(uint8_t reg)
{
uint8_t v = 0;
readRegs(reg, &v, 1);
return v;
}
inline bool Imu::selectBank(uint8_t bank)
{
return writeReg(REG_BANK_SEL, (uint8_t)(bank << 4));
}
inline bool Imu::begin(const Config &c)
{
cfg_ = c;
fd_ = ::open(c.i2c_path, O_RDWR);
if (fd_ < 0) {
if (c.verbose)
std::fprintf(stderr, "[icm20948] cannot open %s: %m\n", c.i2c_path);
return false;
}
// Auto-detect I2C address (AD0 low → 0x68, AD0 high → 0x69)
bool found = false;
for (uint8_t a : {(uint8_t)0x68, (uint8_t)0x69}) {
addr_ = a;
// Ensure bank 0 before reading WHO_AM_I
writeReg(REG_BANK_SEL, 0x00);
uint8_t id = readReg(REG_WHO_AM_I);
if (c.verbose)
std::fprintf(stderr, "[icm20948] probe 0x%02X → WHO_AM_I = 0x%02X\n", a, id);
if (id == WHO_AM_I_VAL) { found = true; break; }
}
if (!found) {
if (c.verbose)
std::fprintf(stderr, "[icm20948] chip not found on %s at 0x68 or 0x69\n"
" check wiring, I2C enabled, pull-ups\n",
c.i2c_path);
::close(fd_); fd_ = -1;
return false;
}
if (c.verbose)
std::fprintf(stderr, "[icm20948] found at 0x%02X\n", addr_);
// Device reset — sets all registers to default values.
selectBank(0);
writeReg(REG_PWR_MGMT_1, 0x80); // DEVICE_RESET bit
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Wait until reset is complete (DEVICE_RESET bit self-clears).
for (int i = 0; i < 50; ++i) {
if (!(readReg(REG_PWR_MGMT_1) & 0x80)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Verify still alive after reset.
if (readReg(REG_WHO_AM_I) != WHO_AM_I_VAL) {
if (c.verbose)
std::fprintf(stderr, "[icm20948] WHO_AM_I mismatch after reset\n");
::close(fd_); fd_ = -1;
return false;
}
// Wake up: auto-select clock source (best practice per datasheet).
writeReg(REG_PWR_MGMT_1, 0x01);
std::this_thread::sleep_for(std::chrono::milliseconds(30));
// Enable all accel + gyro axes.
writeReg(REG_PWR_MGMT_2, 0x00);
// Configure full-scale in Bank 2.
accel_fs_ = c.accel_fs & 0x03;
gyro_fs_ = c.gyro_fs & 0x03;
selectBank(2);
writeReg(B2_ACCEL_CONFIG, (uint8_t)(accel_fs_ << 2));
writeReg(B2_GYRO_CONFIG_1, (uint8_t)(gyro_fs_ << 2));
selectBank(0); // leave in bank 0 for normal operation
if (c.verbose) {
static const unsigned gyro_dps[] = { 250u, 500u, 1000u, 2000u };
std::fprintf(stderr, "[icm20948] init OK: accel ±%ug gyro ±%udps\n",
(unsigned)(2u << accel_fs_), gyro_dps[gyro_fs_]);
}
return true;
}
inline void Imu::end()
{
if (fd_ >= 0) { ::close(fd_); fd_ = -1; }
}
inline bool Imu::read(RawSample &raw)
{
selectBank(0);
uint8_t buf[14]{};
if (!readRegs(REG_ACCEL_XOUT_H, buf, 14)) return false;
auto s16 = [](uint8_t hi, uint8_t lo) -> int16_t {
return (int16_t)((uint16_t)hi << 8 | lo);
};
raw.ax = s16(buf[0], buf[1]);
raw.ay = s16(buf[2], buf[3]);
raw.az = s16(buf[4], buf[5]);
raw.temp_raw = s16(buf[6], buf[7]);
raw.gx = s16(buf[8], buf[9]);
raw.gy = s16(buf[10], buf[11]);
raw.gz = s16(buf[12], buf[13]);
return true;
}
inline bool Imu::read(Sample &s)
{
RawSample raw{};
if (!read(raw)) return false;
float as = ACCEL_SCALE[accel_fs_];
float gs = GYRO_SCALE[gyro_fs_];
s.ax = raw.ax * as;
s.ay = raw.ay * as;
s.az = raw.az * as;
s.gx = raw.gx * gs;
s.gy = raw.gy * gs;
s.gz = raw.gz * gs;
// Temp formula from ICM-20948 datasheet: T°C = (raw - 0) / 333.87 + 21.0
s.temp_c = (float)raw.temp_raw / 333.87f + 21.0f;
return true;
}
} // namespace icm20948

View File

@ -0,0 +1,69 @@
// imu_test.cpp — ICM-20948 IMU test
// Usage: sudo ./imu_test [-v] [-r] [-n COUNT]
// -v verbose init debug
// -r print raw 16-bit values instead of scaled
// -n COUNT exit after COUNT samples (default: run forever)
//
// If chip not found:
// sudo i2cdetect -y 1 ← check 0x68 or 0x69 appears
// If nothing shows: check wiring, I2C enabled in raspi-config
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <chrono>
#include "icm20948.hpp"
int main(int argc, char **argv)
{
icm20948::Config cfg;
cfg.verbose = false;
bool raw_mode = false;
int count = -1;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-v") == 0) {
cfg.verbose = true;
} else if (std::strcmp(argv[i], "-r") == 0) {
raw_mode = true;
} else if (std::strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
count = std::atoi(argv[++i]);
}
}
icm20948::Imu imu;
if (!imu.begin(cfg)) {
std::fprintf(stderr, "ERROR: IMU init failed\n"
" Check: I2C enabled? (sudo raspi-config)\n"
" Check: sudo i2cdetect -y 1\n"
" Check: wiring SDA=GPIO2 SCL=GPIO3\n"
" Check: AD0 pin → 0x68 (GND) or 0x69 (VCC)\n"
" Run with -v for detailed debug output\n");
return 1;
}
std::printf("IMU OK at 0x%02X — reading at 10 Hz%s\n\n",
imu.address(), raw_mode ? " (raw mode)" : "");
for (int n = 0; count < 0 || n < count; ++n) {
if (raw_mode) {
icm20948::RawSample raw{};
if (!imu.read(raw)) { std::fprintf(stderr, "read error\n"); break; }
std::printf("ax=%6d ay=%6d az=%6d | gx=%6d gy=%6d gz=%6d | t_raw=%6d\n",
raw.ax, raw.ay, raw.az, raw.gx, raw.gy, raw.gz, raw.temp_raw);
} else {
icm20948::Sample s{};
if (!imu.read(s)) { std::fprintf(stderr, "read error\n"); break; }
std::printf("a[g] %+7.3f %+7.3f %+7.3f | "
"g[°/s] %+8.2f %+8.2f %+8.2f | "
"T %.1f°C\n",
s.ax, s.ay, s.az,
s.gx, s.gy, s.gz,
s.temp_c);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
imu.end();
return 0;
}

View File

@ -0,0 +1,95 @@
// lora_rx.cpp — LR1121 receive test
// Usage: sudo ./lora_rx [-v] [--433|--868|--24|freq_hz] [--reset]
// -v verbose step labels (shows exactly where init hangs)
// --433 433.05 MHz (default)
// --868 868 MHz
// --24 2403 MHz
// freq_hz any raw frequency in Hz
// --reset send Reboot(app) to escape bootloader, print fw before/after, exit
//
// TX and RX must use identical SF/BW/CR/freq settings.
// Requires SPI: sudo raspi-config → Interfaces → SPI → Yes → reboot
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "lr1121_malnus.hpp"
int main(int argc, char **argv)
{
lr1121::Config cfg;
cfg.verbose = false;
cfg.pa_sel = 0x01;
cfg.tx_dbm = 14;
cfg.sf = 0x07; // SF7
cfg.bw = 0x04; // 125 kHz
cfg.cr = 0x01; // CR 4/5
bool do_reset = false;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-v") == 0) cfg.verbose = true;
else if (std::strcmp(argv[i], "--433") == 0) cfg.freq_hz = lr1121::FREQ_433;
else if (std::strcmp(argv[i], "--868") == 0) cfg.freq_hz = lr1121::FREQ_868;
else if (std::strcmp(argv[i], "--24") == 0 ||
std::strcmp(argv[i], "--2g4") == 0) cfg.freq_hz = lr1121::FREQ_2400;
else if (std::strcmp(argv[i], "--reset") == 0) do_reset = true;
else cfg.freq_hz = (uint32_t)std::strtoul(argv[i], nullptr, 10);
}
std::printf("lora_rx: %u Hz SF%u BW=0x%02X%s\n",
cfg.freq_hz, cfg.sf, cfg.bw,
cfg.verbose ? " [verbose]" : "");
if (do_reset) {
cfg.verbose = true;
lr1121::Radio radio;
if (!radio.beginRaw(cfg)) {
std::fprintf(stderr, "ERROR: cannot open SPI/GPIO\n");
return 1;
}
auto v1 = radio.getVersion();
std::printf("before reboot: hw=0x%02X type=0x%02X fw=0x%02X%02X\n",
v1.hw, v1.type, v1.fw_hi, v1.fw_lo);
std::printf("sending Reboot(app)...\n");
radio.reboot(false);
auto v2 = radio.getVersion();
std::printf("after reboot: hw=0x%02X type=0x%02X fw=0x%02X%02X\n",
v2.hw, v2.type, v2.fw_hi, v2.fw_lo);
if (v2.fw_hi >= 0x02)
std::printf("OK — application firmware active (fw >= 0x02xx)\n"
"Run without --reset to continue.\n");
else
std::printf("Still in bootloader (fw=0x%02X%02X).\n",
v2.fw_hi, v2.fw_lo);
radio.end();
return 0;
}
lr1121::Radio radio;
if (!radio.begin(cfg)) {
std::fprintf(stderr, "ERROR: radio init failed\n"
" Check: SPI enabled? wiring? DIO5/DIO6 connected?\n"
" Run with -v for step-by-step output\n");
return 1;
}
std::printf("Radio OK — listening (Ctrl+C to stop)\n\n");
uint8_t buf[256];
int pkt = 0;
for (;;) {
lr1121::RxInfo info{};
int r = radio.receive(buf, (uint8_t)(sizeof(buf) - 1), 30'000, &info);
if (r > 0) {
buf[r] = '\0';
std::printf("[%4d] %d B rssi=%d dBm snr=%d dB '%s'\n",
++pkt, r, info.rssi_dbm, info.snr_db, buf);
} else if (r == -1) {
std::printf(" timeout (30s), still listening...\n");
} else if (r == -2) {
std::printf(" CRC error\n");
}
}
radio.end();
return 0;
}

View File

@ -0,0 +1,60 @@
// lora_tx.cpp — LR1121 transmit test
// Usage: sudo ./lora_tx [-v] [--433|--868|--24|freq_hz]
// -v verbose step labels (shows exactly where init hangs)
// --433 433.05 MHz (default)
// --868 868 MHz
// --24 2403 MHz
// freq_hz any raw frequency in Hz
//
// Requires SPI: sudo raspi-config → Interfaces → SPI → Yes → reboot
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <chrono>
#include "lr1121_malnus.hpp"
int main(int argc, char **argv)
{
lr1121::Config cfg;
cfg.verbose = false;
cfg.pa_sel = 0x01; // HP PA — most modules; change to 0x00 for LP
cfg.tx_dbm = 14;
cfg.sf = 0x07; // SF7
cfg.bw = 0x04; // 125 kHz
cfg.cr = 0x01; // CR 4/5
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-v") == 0) cfg.verbose = true;
else if (std::strcmp(argv[i], "--433") == 0) cfg.freq_hz = lr1121::FREQ_433;
else if (std::strcmp(argv[i], "--868") == 0) cfg.freq_hz = lr1121::FREQ_868;
else if (std::strcmp(argv[i], "--24") == 0 ||
std::strcmp(argv[i], "--2g4") == 0) cfg.freq_hz = lr1121::FREQ_2400;
else cfg.freq_hz = (uint32_t)std::strtoul(argv[i], nullptr, 10);
}
std::printf("lora_tx: %u Hz SF%u BW=0x%02X PA=%s%s\n",
cfg.freq_hz, cfg.sf, cfg.bw,
cfg.pa_sel ? "HP" : "LP",
cfg.verbose ? " [verbose]" : "");
lr1121::Radio radio;
if (!radio.begin(cfg)) {
std::fprintf(stderr, "ERROR: radio init failed\n"
" Check: SPI enabled? wiring? DIO5/DIO6 connected?\n"
" Run with -v for step-by-step output\n");
return 1;
}
std::printf("Radio OK — sending every second\n");
for (int n = 0; ; ++n) {
char msg[32];
int len = std::snprintf(msg, sizeof(msg), "hello %d", n);
bool ok = radio.send((const uint8_t *)msg, (uint8_t)len);
std::printf("[%4d] tx '%s' → %s\n", n, msg, ok ? "OK" : "TIMEOUT");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
radio.end();
return 0;
}

View File

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

View File

@ -30,7 +30,7 @@ GLuint compile(GLenum t, const std::string& src)
if (!ok) {
char log[1024];
glGetShaderInfoLog(s, sizeof(log), NULL, log);
std::cout << "shader compile failed:\n" << log;
std::cerr << "shader compile failed:\n" << log;
}
return s;
}
@ -51,8 +51,8 @@ int main()
if(!img1 || !img2)
{
std::cout << "image load failed\n";
return 0;
std::cerr << "image load failed\n";
return 1;
}
// ---------------- TEXTURES ----------------
@ -99,7 +99,8 @@ int main()
if (!linked) {
char log[1024];
glGetProgramInfoLog(prog, sizeof(log), NULL, log);
std::cout << "program link failed:\n" << log;
std::cerr << "program link failed:\n" << log;
return 1;
}
// ---------------- QUAD (FIXED) ----------------
@ -133,8 +134,10 @@ int main()
glBindFramebuffer(GL_FRAMEBUFFER,fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,outTex,0);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "FBO broken\n";
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "FBO broken\n";
return 1;
}
// ---------------- RENDER ----------------
glUseProgram(prog);