559 lines
19 KiB
C++
559 lines
19 KiB
C++
// lr1121_malnus.hpp — minimal LR1121 LoRa driver for this board
|
|
//
|
|
// Board wiring:
|
|
// SPI : /dev/spidev0.0 (NSS GPIO8)
|
|
// BUSY : GPIO24
|
|
// RESET: GPIO25
|
|
// RF switch controlled by LR1121 DIO5/DIO6
|
|
//
|
|
// Command set references:
|
|
// - Semtech LR1121 user manual rev 1.2
|
|
// - Semtech SWDR001
|
|
// - RadioLib LR11x0 behavior
|
|
#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 {
|
|
|
|
constexpr uint16_t OC_GET_VERSION = 0x0101;
|
|
constexpr uint16_t OC_WRITE_BUF8 = 0x0109;
|
|
constexpr uint16_t OC_READ_BUF8 = 0x010A;
|
|
constexpr uint16_t OC_CLEAR_RXBUF = 0x010B;
|
|
constexpr uint16_t OC_GET_ERRORS = 0x010D;
|
|
constexpr uint16_t OC_CLEAR_ERRORS = 0x010E;
|
|
constexpr uint16_t OC_CALIBRATE = 0x010F;
|
|
constexpr uint16_t OC_SET_REGMODE = 0x0110;
|
|
constexpr uint16_t OC_CALIBRATE_IMAGE = 0x0111;
|
|
constexpr uint16_t OC_SET_DIO_AS_RFSW = 0x0112;
|
|
constexpr uint16_t OC_SET_DIOIRQ = 0x0113;
|
|
constexpr uint16_t OC_CLEAR_IRQ = 0x0114;
|
|
constexpr uint16_t OC_REBOOT = 0x0118;
|
|
constexpr uint16_t OC_GET_VBAT = 0x0119;
|
|
constexpr uint16_t OC_SET_STANDBY = 0x011C;
|
|
|
|
constexpr uint16_t OC_GET_RXBUF_STA = 0x0203;
|
|
constexpr uint16_t OC_GET_PKT_STATUS = 0x0204;
|
|
constexpr uint16_t OC_SET_LORA_NET = 0x0208;
|
|
constexpr uint16_t OC_SET_RX = 0x0209;
|
|
constexpr uint16_t OC_SET_TX = 0x020A;
|
|
constexpr uint16_t OC_SET_RF_FREQ = 0x020B;
|
|
constexpr uint16_t OC_SET_PKT_TYPE = 0x020E;
|
|
constexpr uint16_t OC_SET_MOD_PARAM = 0x020F;
|
|
constexpr uint16_t OC_SET_PKT_PARAM = 0x0210;
|
|
constexpr uint16_t OC_SET_TX_PARAMS = 0x0211;
|
|
constexpr uint16_t OC_SET_PKT_ADRS = 0x0212;
|
|
constexpr uint16_t OC_SET_FALLBACK_MODE = 0x0213;
|
|
constexpr uint16_t OC_SET_PA_CFG = 0x0215;
|
|
|
|
constexpr uint8_t CALIB_ALL = 0x3F;
|
|
|
|
constexpr 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_LBD = (1u << 21);
|
|
constexpr uint32_t IRQ_ALL = 0x1BF80FFCu;
|
|
|
|
constexpr uint32_t FREQ_433 = 433'050'000u;
|
|
constexpr uint32_t FREQ_868 = 868'000'000u;
|
|
constexpr uint32_t FREQ_2400 = 2'403'000'000u;
|
|
|
|
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;
|
|
unsigned reset_gpio = 25;
|
|
unsigned dio9_gpio = 4; // IRQ line from LR1121
|
|
uint32_t freq_hz = FREQ_433;
|
|
uint8_t sf = 0x07;
|
|
uint8_t bw = 0x04;
|
|
uint8_t cr = 0x01;
|
|
int8_t tx_dbm = 10;
|
|
uint8_t pa_sel = 0x00; // LP default to avoid LBD on weak VBAT rails
|
|
uint8_t pa_supply = 0x00;
|
|
bool use_dcdc = false;
|
|
bool lora_wan = false;
|
|
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;
|
|
uint8_t fw_hi;
|
|
uint8_t fw_lo;
|
|
};
|
|
|
|
class Radio {
|
|
public:
|
|
bool begin(const Config &cfg);
|
|
bool beginRaw(const Config &cfg);
|
|
void end();
|
|
|
|
bool send(const uint8_t *data, uint8_t n);
|
|
int receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info = nullptr);
|
|
|
|
ChipVersion getVersion();
|
|
void reboot(bool stay_in_bootloader = false);
|
|
|
|
private:
|
|
Config cfg_{};
|
|
int spi_fd_ = -1;
|
|
int busy_fd_ = -1;
|
|
int reset_fd_ = -1;
|
|
int dio9_fd_ = -1;
|
|
|
|
bool openSpi(const Config &cfg);
|
|
bool openGpio(unsigned line, bool out, int &fd_out);
|
|
int getGpioLine(int fd);
|
|
void setGpioLine(int fd, int value);
|
|
void hardReset();
|
|
bool waitBusy(int timeout_ms = 1000);
|
|
|
|
bool spiTransfer(uint8_t *buf, size_t len);
|
|
bool wcmd(uint16_t op, const uint8_t *params = nullptr, size_t n = 0);
|
|
bool rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr);
|
|
|
|
bool setStandbyXosc();
|
|
bool setStandbyRc();
|
|
bool setIrqMask(uint32_t irq1, uint32_t irq2 = 0);
|
|
bool clearIrq(uint32_t mask = IRQ_ALL);
|
|
uint32_t getIrq();
|
|
uint16_t getErrors();
|
|
|
|
static void imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2);
|
|
static uint8_t computeLdRo(uint8_t sf, uint8_t bw);
|
|
static uint32_t timeoutMsToRtcSteps(uint32_t ms);
|
|
static void computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max);
|
|
};
|
|
|
|
inline bool Radio::spiTransfer(uint8_t *buf, size_t len)
|
|
{
|
|
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>(len);
|
|
tr.speed_hz = cfg_.spi_hz;
|
|
tr.bits_per_word = 8;
|
|
return ioctl(spi_fd_, SPI_IOC_MESSAGE(1), &tr) >= 0;
|
|
}
|
|
|
|
inline bool Radio::wcmd(uint16_t op, const uint8_t *params, size_t n)
|
|
{
|
|
if (!waitBusy()) return false;
|
|
uint8_t cmd[258]{};
|
|
cmd[0] = static_cast<uint8_t>(op >> 8);
|
|
cmd[1] = static_cast<uint8_t>(op & 0xFF);
|
|
if (params && n) std::memcpy(cmd + 2, params, n);
|
|
if (!spiTransfer(cmd, 2 + n)) return false;
|
|
return waitBusy();
|
|
}
|
|
|
|
inline bool Radio::rcmd(uint16_t op, const uint8_t *params, size_t np, uint8_t *out, size_t nr)
|
|
{
|
|
if (!waitBusy()) return false;
|
|
uint8_t cmd[32]{};
|
|
cmd[0] = static_cast<uint8_t>(op >> 8);
|
|
cmd[1] = static_cast<uint8_t>(op & 0xFF);
|
|
if (params && np) std::memcpy(cmd + 2, params, np);
|
|
if (!spiTransfer(cmd, 2 + np)) return false;
|
|
|
|
if (!waitBusy()) return false;
|
|
uint8_t rsp[260]{};
|
|
if (!spiTransfer(rsp, nr + 1)) return false;
|
|
std::memcpy(out, rsp + 1, nr);
|
|
return true;
|
|
}
|
|
|
|
inline bool Radio::setIrqMask(uint32_t irq1, uint32_t irq2)
|
|
{
|
|
const uint8_t p[8] = {
|
|
static_cast<uint8_t>(irq1 >> 24), static_cast<uint8_t>(irq1 >> 16),
|
|
static_cast<uint8_t>(irq1 >> 8), static_cast<uint8_t>(irq1),
|
|
static_cast<uint8_t>(irq2 >> 24), static_cast<uint8_t>(irq2 >> 16),
|
|
static_cast<uint8_t>(irq2 >> 8), static_cast<uint8_t>(irq2),
|
|
};
|
|
return wcmd(OC_SET_DIOIRQ, p, sizeof(p));
|
|
}
|
|
|
|
inline bool Radio::clearIrq(uint32_t mask)
|
|
{
|
|
const uint8_t p[4] = {
|
|
static_cast<uint8_t>(mask >> 24), static_cast<uint8_t>(mask >> 16),
|
|
static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask),
|
|
};
|
|
return wcmd(OC_CLEAR_IRQ, p, sizeof(p));
|
|
}
|
|
|
|
inline uint32_t Radio::getIrq()
|
|
{
|
|
uint8_t b[6]{};
|
|
if (!spiTransfer(b, sizeof(b))) return 0;
|
|
return (static_cast<uint32_t>(b[2]) << 24) |
|
|
(static_cast<uint32_t>(b[3]) << 16) |
|
|
(static_cast<uint32_t>(b[4]) << 8) |
|
|
(static_cast<uint32_t>(b[5]));
|
|
}
|
|
|
|
inline uint16_t Radio::getErrors()
|
|
{
|
|
uint8_t e[2]{};
|
|
if (!rcmd(OC_GET_ERRORS, nullptr, 0, e, sizeof(e))) return 0xFFFF;
|
|
return static_cast<uint16_t>((e[0] << 8) | e[1]);
|
|
}
|
|
|
|
inline bool Radio::setStandbyRc()
|
|
{
|
|
const uint8_t p[] = {0x00};
|
|
return wcmd(OC_SET_STANDBY, p, sizeof(p));
|
|
}
|
|
|
|
inline bool Radio::setStandbyXosc()
|
|
{
|
|
const uint8_t p[] = {0x01};
|
|
return wcmd(OC_SET_STANDBY, p, sizeof(p));
|
|
}
|
|
|
|
inline bool Radio::openSpi(const Config &cfg)
|
|
{
|
|
spi_fd_ = ::open(cfg.spi_path, O_RDWR);
|
|
if (spi_fd_ < 0) return false;
|
|
|
|
uint8_t mode = SPI_MODE_0;
|
|
uint8_t bits = 8;
|
|
if (ioctl(spi_fd_, SPI_IOC_WR_MODE, &mode) < 0) return false;
|
|
if (ioctl(spi_fd_, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) return false;
|
|
if (ioctl(spi_fd_, SPI_IOC_WR_MAX_SPEED_HZ, &cfg.spi_hz) < 0) return false;
|
|
return true;
|
|
}
|
|
|
|
inline bool Radio::openGpio(unsigned line, bool out, int &fd_out)
|
|
{
|
|
int chip = ::open(cfg_.gpio_chip, O_RDWR);
|
|
if (chip < 0) 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);
|
|
const int rc = ioctl(chip, GPIO_GET_LINEHANDLE_IOCTL, &req);
|
|
::close(chip);
|
|
if (rc < 0) return false;
|
|
|
|
fd_out = req.fd;
|
|
return true;
|
|
}
|
|
|
|
inline int Radio::getGpioLine(int fd)
|
|
{
|
|
gpiohandle_data d{};
|
|
if (ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &d) < 0) return 1;
|
|
return d.values[0];
|
|
}
|
|
|
|
inline void Radio::setGpioLine(int fd, int value)
|
|
{
|
|
gpiohandle_data d{};
|
|
d.values[0] = value;
|
|
(void)ioctl(fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &d);
|
|
}
|
|
|
|
inline void Radio::hardReset()
|
|
{
|
|
setGpioLine(reset_fd_, 0);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
setGpioLine(reset_fd_, 1);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
|
|
inline bool Radio::waitBusy(int timeout_ms)
|
|
{
|
|
const int loops = timeout_ms * 20;
|
|
for (int i = 0; i < loops; ++i) {
|
|
if (getGpioLine(busy_fd_) == 0) return true;
|
|
std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
inline void Radio::imgCalFreqs(uint32_t hz, uint8_t &f1, uint8_t &f2)
|
|
{
|
|
const uint32_t mhz = hz / 1'000'000u;
|
|
uint32_t lo = 430, hi = 440;
|
|
if (mhz < 446) {
|
|
lo = 430; hi = 440;
|
|
} else if (mhz < 740) {
|
|
lo = 470; hi = 510;
|
|
} else if (mhz < 890) {
|
|
lo = 860; hi = 876;
|
|
} else if (mhz < 2000) {
|
|
lo = 902; hi = 928;
|
|
} else {
|
|
lo = 2400; hi = 2500;
|
|
}
|
|
f1 = static_cast<uint8_t>(lo / 4);
|
|
f2 = static_cast<uint8_t>((hi + 3) / 4);
|
|
}
|
|
|
|
inline uint8_t Radio::computeLdRo(uint8_t sf, uint8_t bw)
|
|
{
|
|
static const uint32_t bw_hz[] = {0, 15625, 31250, 62500, 125000, 250000, 500000};
|
|
const uint32_t bw_val = (bw < 7) ? bw_hz[bw] : 125000;
|
|
const uint32_t sym_ms = (1000u << sf) / bw_val;
|
|
return (sym_ms > 16) ? 1 : 0;
|
|
}
|
|
|
|
inline uint32_t Radio::timeoutMsToRtcSteps(uint32_t ms)
|
|
{
|
|
if (ms == 0) return 0x00FFFFFFu;
|
|
const uint64_t steps = (static_cast<uint64_t>(ms) * 32768u) / 1000u;
|
|
return (steps > 0x00FFFFFFu) ? 0x00FFFFFFu : static_cast<uint32_t>(steps);
|
|
}
|
|
|
|
inline void Radio::computePaConfig(uint8_t pa_sel, int8_t dbm, uint8_t &duty, uint8_t &hp_max)
|
|
{
|
|
if (pa_sel == 0x01) {
|
|
if (dbm >= 22) { duty = 4; hp_max = 7; }
|
|
else if (dbm >= 20) { duty = 3; hp_max = 5; }
|
|
else if (dbm >= 17) { duty = 2; hp_max = 3; }
|
|
else if (dbm >= 14) { duty = 2; hp_max = 2; }
|
|
else if (dbm >= 10) { duty = 1; hp_max = 1; }
|
|
else { duty = 0; hp_max = 0; }
|
|
} else {
|
|
hp_max = 0;
|
|
if (dbm >= 14) duty = 7;
|
|
else if (dbm >= 10) duty = 4;
|
|
else if (dbm >= 0) duty = 2;
|
|
else duty = 0;
|
|
}
|
|
}
|
|
|
|
inline ChipVersion Radio::getVersion()
|
|
{
|
|
uint8_t v[4]{};
|
|
(void)rcmd(OC_GET_VERSION, nullptr, 0, v, sizeof(v));
|
|
return {v[0], v[1], v[2], v[3]};
|
|
}
|
|
|
|
inline bool Radio::beginRaw(const Config &cfg)
|
|
{
|
|
cfg_ = cfg;
|
|
if (!openSpi(cfg_)) return false;
|
|
if (!openGpio(cfg_.reset_gpio, true, reset_fd_)) return false;
|
|
if (!openGpio(cfg_.busy_gpio, false, busy_fd_)) return false;
|
|
if (!openGpio(cfg_.dio9_gpio, false, dio9_fd_)) return false;
|
|
hardReset();
|
|
return waitBusy(500);
|
|
}
|
|
|
|
inline bool Radio::begin(const Config &cfg)
|
|
{
|
|
if (!beginRaw(cfg)) return false;
|
|
|
|
const ChipVersion v = getVersion();
|
|
if (cfg_.verbose) {
|
|
std::fprintf(stderr, "[lr1121] hw=0x%02X type=0x%02X fw=0x%02X%02X\n", v.hw, v.type, v.fw_hi, v.fw_lo);
|
|
uint8_t raw = 0;
|
|
if (rcmd(OC_GET_VBAT, nullptr, 0, &raw, 1)) {
|
|
const float vb = raw / 34.0f;
|
|
std::fprintf(stderr, "[lr1121] VBAT raw=0x%02X (~%.2fV)\n", raw, vb);
|
|
}
|
|
}
|
|
if (v.type != 0x03) return false;
|
|
|
|
if (!setStandbyRc()) return false;
|
|
{ const uint8_t p[] = {0x01}; if (!wcmd(OC_SET_FALLBACK_MODE, p, sizeof(p))) return false; }
|
|
if (!clearIrq()) return false;
|
|
if (!setIrqMask(0, 0)) return false;
|
|
{ const uint8_t p[] = {CALIB_ALL}; if (!wcmd(OC_CALIBRATE, p, sizeof(p))) return false; }
|
|
{ const uint8_t p[] = {0x00}; (void)wcmd(OC_CLEAR_ERRORS, p, 0); }
|
|
|
|
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.use_dcdc ? 0x01 : 0x00)}; if (!wcmd(OC_SET_REGMODE, p, sizeof(p))) return false; }
|
|
{ const uint8_t p[] = {0x02}; if (!wcmd(OC_SET_PKT_TYPE, p, sizeof(p))) return false; }
|
|
{
|
|
const uint8_t p[] = {
|
|
static_cast<uint8_t>(cfg_.freq_hz >> 24), static_cast<uint8_t>(cfg_.freq_hz >> 16),
|
|
static_cast<uint8_t>(cfg_.freq_hz >> 8), static_cast<uint8_t>(cfg_.freq_hz),
|
|
};
|
|
if (!wcmd(OC_SET_RF_FREQ, p, sizeof(p))) return false;
|
|
}
|
|
{
|
|
uint8_t f1 = 0, f2 = 0;
|
|
imgCalFreqs(cfg_.freq_hz, f1, f2);
|
|
const uint8_t p[] = {f1, f2};
|
|
if (!wcmd(OC_CALIBRATE_IMAGE, p, sizeof(p))) return false;
|
|
}
|
|
{ const uint8_t p[] = {0x00, 0x00}; if (!wcmd(OC_SET_PKT_ADRS, p, sizeof(p))) return false; }
|
|
{
|
|
const uint8_t p[] = {cfg_.sf, cfg_.bw, cfg_.cr, computeLdRo(cfg_.sf, cfg_.bw)};
|
|
if (!wcmd(OC_SET_MOD_PARAM, p, sizeof(p))) return false;
|
|
}
|
|
{ const uint8_t p[] = {0x00, 0x08, 0x00, 0xFF, 0x01, 0x00}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; }
|
|
|
|
{
|
|
uint8_t duty = 0, hp_max = 0;
|
|
computePaConfig(cfg_.pa_sel, cfg_.tx_dbm, duty, hp_max);
|
|
const uint8_t p[] = {cfg_.pa_sel, cfg_.pa_supply, duty, hp_max};
|
|
if (!wcmd(OC_SET_PA_CFG, p, sizeof(p))) return false;
|
|
if (cfg_.verbose) {
|
|
std::fprintf(stderr, "[lr1121] PA: sel=0x%02X supply=0x%02X duty=%u hp_max=%u dbm=%d\n",
|
|
cfg_.pa_sel, cfg_.pa_supply, duty, hp_max, static_cast<int>(cfg_.tx_dbm));
|
|
}
|
|
}
|
|
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.tx_dbm), 0x02}; if (!wcmd(OC_SET_TX_PARAMS, p, sizeof(p))) return false; }
|
|
{ const uint8_t p[] = {static_cast<uint8_t>(cfg_.lora_wan ? 0x01 : 0x00)}; if (!wcmd(OC_SET_LORA_NET, p, sizeof(p))) return false; }
|
|
|
|
// DIO5/DIO6 RF switch mapping: STBY(0,0), RX(1,0), TX(0,1), TX_HP(0,1)
|
|
{ const uint8_t p[] = {0x03, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00, 0x00}; if (!wcmd(OC_SET_DIO_AS_RFSW, p, sizeof(p))) return false; }
|
|
|
|
if (!setStandbyXosc()) return false;
|
|
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] init OK: %u Hz SF%u BW=0x%02X CR=0x%02X\n", cfg_.freq_hz, cfg_.sf, cfg_.bw, cfg_.cr);
|
|
return true;
|
|
}
|
|
|
|
inline void Radio::reboot(bool stay_in_bootloader)
|
|
{
|
|
const uint8_t p[] = { static_cast<uint8_t>(stay_in_bootloader ? 0x01 : 0x00) };
|
|
(void)wcmd(OC_REBOOT, p, sizeof(p));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
}
|
|
|
|
inline void Radio::end()
|
|
{
|
|
if (spi_fd_ >= 0) { ::close(spi_fd_); spi_fd_ = -1; }
|
|
if (busy_fd_ >= 0) { ::close(busy_fd_); busy_fd_ = -1; }
|
|
if (reset_fd_ >= 0) { ::close(reset_fd_); reset_fd_ = -1; }
|
|
if (dio9_fd_ >= 0) { ::close(dio9_fd_); dio9_fd_ = -1; }
|
|
}
|
|
|
|
inline bool Radio::send(const uint8_t *data, uint8_t n)
|
|
{
|
|
if (n == 0) return false;
|
|
if (!setStandbyXosc()) return false;
|
|
if (!clearIrq()) return false;
|
|
if (!setIrqMask(IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD, IRQ_TX_DONE | IRQ_TIMEOUT | IRQ_LBD)) return false;
|
|
|
|
if (!wcmd(OC_WRITE_BUF8, data, n)) return false;
|
|
{ const uint8_t p[] = {0x00, 0x08, 0x00, n, 0x01, 0x00}; if (!wcmd(OC_SET_PKT_PARAM, p, sizeof(p))) return false; }
|
|
|
|
{
|
|
const uint32_t tx_steps = timeoutMsToRtcSteps(3000);
|
|
const uint8_t p[] = {
|
|
static_cast<uint8_t>(tx_steps >> 16),
|
|
static_cast<uint8_t>(tx_steps >> 8),
|
|
static_cast<uint8_t>(tx_steps),
|
|
};
|
|
if (!wcmd(OC_SET_TX, p, sizeof(p))) return false;
|
|
}
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(3500);
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
// Primary trigger is GPIO DIO9 (actual IRQ pin), then decode IRQ bits.
|
|
if (getGpioLine(dio9_fd_) == 0) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
const uint32_t irq = getIrq();
|
|
if (irq & IRQ_TX_DONE) {
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return true;
|
|
}
|
|
if (irq & (IRQ_TIMEOUT | IRQ_LBD)) {
|
|
if (cfg_.verbose) {
|
|
std::fprintf(stderr, "[lr1121] TX fail irq=0x%08X errs=0x%04X\n", irq, getErrors());
|
|
if (irq & IRQ_LBD) std::fprintf(stderr, "[lr1121] LBD triggered: reduce TX power or use LP PA\n");
|
|
}
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return false;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
|
|
if (cfg_.verbose) std::fprintf(stderr, "[lr1121] TX poll timeout errs=0x%04X\n", getErrors());
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return false;
|
|
}
|
|
|
|
inline int Radio::receive(uint8_t *buf, uint8_t cap, uint32_t timeout_ms, RxInfo *rx_info)
|
|
{
|
|
if (!setStandbyXosc()) return -1;
|
|
if (!clearIrq()) return -1;
|
|
if (!setIrqMask(IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT, IRQ_RX_DONE | IRQ_CRC_ERR | IRQ_TIMEOUT)) return -1;
|
|
|
|
const uint32_t steps = timeoutMsToRtcSteps(timeout_ms);
|
|
{
|
|
const uint8_t p[] = {
|
|
static_cast<uint8_t>(steps >> 16),
|
|
static_cast<uint8_t>(steps >> 8),
|
|
static_cast<uint8_t>(steps),
|
|
};
|
|
if (!wcmd(OC_SET_RX, p, sizeof(p))) return -1;
|
|
}
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms == 0 ? 60000 : timeout_ms + 500);
|
|
for (;;) {
|
|
if (std::chrono::steady_clock::now() >= deadline) {
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return -1;
|
|
}
|
|
if (getGpioLine(dio9_fd_) == 0) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
const uint32_t irq = getIrq();
|
|
if (irq & IRQ_TIMEOUT) {
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return -1;
|
|
}
|
|
if ((irq & (IRQ_RX_DONE | IRQ_CRC_ERR)) == 0) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
|
|
if (rx_info) {
|
|
uint8_t p[3]{};
|
|
if (rcmd(OC_GET_PKT_STATUS, nullptr, 0, p, sizeof(p))) {
|
|
rx_info->rssi_dbm = static_cast<int8_t>(-(int)p[0] / 2);
|
|
rx_info->snr_db = static_cast<int8_t>(static_cast<int8_t>(p[1]) / 4);
|
|
rx_info->signal_rssi_dbm = static_cast<int8_t>(-(int)p[2] / 2);
|
|
}
|
|
}
|
|
|
|
uint8_t st[2]{};
|
|
if (!rcmd(OC_GET_RXBUF_STA, nullptr, 0, st, sizeof(st))) return -1;
|
|
const uint8_t len = (st[0] < cap) ? st[0] : cap;
|
|
const uint8_t p[] = {st[1], len};
|
|
if (len > 0 && !rcmd(OC_READ_BUF8, p, sizeof(p), buf, len)) return -1;
|
|
|
|
(void)wcmd(OC_CLEAR_RXBUF, nullptr, 0);
|
|
(void)clearIrq();
|
|
(void)setIrqMask(0, 0);
|
|
return (irq & IRQ_CRC_ERR) ? -2 : static_cast<int>(len);
|
|
}
|
|
}
|
|
|
|
} // namespace lr1121
|