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() int main()
{ {
std::signal(SIGPIPE, SIG_IGN); std::signal(SIGPIPE, SIG_IGN);
std::signal(SIGINT, [](int) { loop.exit(0); });
std::thread(serverThread).detach(); std::thread(serverThread).detach();
auto cm = std::make_unique<CameraManager>(); 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,6 +3,8 @@
Tests for LR1121 LoRa, ICM-20948 IMU, u-blox GPS on Raspberry Pi Zero W 2. Tests for LR1121 LoRa, ICM-20948 IMU, u-blox GPS on Raspberry Pi Zero W 2.
Header-only drivers, kernel ioctls, no external libs. Header-only drivers, kernel ioctls, no external libs.
Driver: `lr1121_malnus.hpp` — crystal oscillator, RF switch via DIO5/DIO6.
--- ---
## Wiring ## Wiring
@ -10,17 +12,23 @@ Header-only drivers, kernel ioctls, no external libs.
### LR1121 (SPI0) ### LR1121 (SPI0)
| Module | Pi GPIO | Pi pin | | Module | Pi GPIO | Pi pin |
|--------|---------|--------| |---------|---------|--------|
| SCK | GPIO11 | 23 | | SCK | GPIO11 | 23 |
| MOSI | GPIO10 | 19 | | MOSI | GPIO10 | 19 |
| MISO | GPIO9 | 21 | | MISO | GPIO9 | 21 |
| NSS | GPIO8 | 24 | | NSS | GPIO8 | 24 |
| BUSY | GPIO24 | 18 | | BUSY | GPIO24 | 18 |
| NRESET | GPIO25 | 22 | | NRESET | GPIO25 | 22 |
| DIO5 | — | chip-driven RF switch (RFSW0) |
| DIO6 | — | chip-driven RF switch (RFSW1) |
| DIO9 | GPIO4 | 7 | | DIO9 | GPIO4 | 7 |
| DIO8 | GPIO23 | 16 | | 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) ### ICM-20948 (I2C1)
@ -63,28 +71,29 @@ sudo ./imu_test -v
## LoRa debug ## LoRa debug
```sh ```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 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 If TX/RX runs but packets never arrive — check that DIO5/DIO6 reach the RF
sudo ./lora_rx -v --433 --tcxo-none # skip TCXO entirely (crystal mode) switch on your module. Without the switch, the antenna path is disconnected.
sudo ./lora_rx -v --433 --tcxo-27 # TCXO 2.7V
sudo ./lora_rx -v --433 --tcxo-33 # TCXO 3.3V (default)
```
The one that gets past "Calibrate done" is your module's config. To try 2.4 GHz instead (different antenna required):
Use the same TCXO flag on TX and RX.
If using the 2.4 GHz antenna instead:
```sh ```sh
sudo ./lora_rx -v --24 sudo ./lora_rx -v --24
sudo ./lora_tx -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 ## 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 -v -a # all sentences
./gps_test -b 9600 # try different baud ./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) { if (!ok) {
char log[1024]; char log[1024];
glGetShaderInfoLog(s, sizeof(log), NULL, log); glGetShaderInfoLog(s, sizeof(log), NULL, log);
std::cout << "shader compile failed:\n" << log; std::cerr << "shader compile failed:\n" << log;
} }
return s; return s;
} }
@ -51,8 +51,8 @@ int main()
if(!img1 || !img2) if(!img1 || !img2)
{ {
std::cout << "image load failed\n"; std::cerr << "image load failed\n";
return 0; return 1;
} }
// ---------------- TEXTURES ---------------- // ---------------- TEXTURES ----------------
@ -99,7 +99,8 @@ int main()
if (!linked) { if (!linked) {
char log[1024]; char log[1024];
glGetProgramInfoLog(prog, sizeof(log), NULL, log); glGetProgramInfoLog(prog, sizeof(log), NULL, log);
std::cout << "program link failed:\n" << log; std::cerr << "program link failed:\n" << log;
return 1;
} }
// ---------------- QUAD (FIXED) ---------------- // ---------------- QUAD (FIXED) ----------------
@ -133,8 +134,10 @@ int main()
glBindFramebuffer(GL_FRAMEBUFFER,fbo); glBindFramebuffer(GL_FRAMEBUFFER,fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,outTex,0); glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,outTex,0);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cout << "FBO broken\n"; std::cerr << "FBO broken\n";
return 1;
}
// ---------------- RENDER ---------------- // ---------------- RENDER ----------------
glUseProgram(prog); glUseProgram(prog);