553 lines
20 KiB
C++
553 lines
20 KiB
C++
// 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
|