diff --git a/chip_test_example/icm20948.hpp b/chip_test_example/icm20948.hpp new file mode 100644 index 0000000..c4494bf --- /dev/null +++ b/chip_test_example/icm20948.hpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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); // 0–3 + +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 = ® + 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 diff --git a/chip_test_example/imu_test.cpp b/chip_test_example/imu_test.cpp new file mode 100644 index 0000000..1b58b89 --- /dev/null +++ b/chip_test_example/imu_test.cpp @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/chip_test_example/lora_rx.cpp b/chip_test_example/lora_rx.cpp new file mode 100644 index 0000000..90573c2 --- /dev/null +++ b/chip_test_example/lora_rx.cpp @@ -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 +#include +#include +#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; +} diff --git a/chip_test_example/lora_tx.cpp b/chip_test_example/lora_tx.cpp new file mode 100644 index 0000000..a9d2005 --- /dev/null +++ b/chip_test_example/lora_tx.cpp @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/chip_test_example/lr1121_malnus.hpp b/chip_test_example/lr1121_malnus.hpp new file mode 100644 index 0000000..5326489 --- /dev/null +++ b/chip_test_example/lr1121_malnus.hpp @@ -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 +#include +#include +#include + +#include +#include +#include +#include +#include + +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(buf); + tr.rx_buf = reinterpret_cast(buf); + tr.len = static_cast(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