Radiolib recording hal tests (#11997)

* test: exercise RadioLib's drivers through a recording HAL, pinned to RadioLib 7.8.0

test_radiolib_drivers drives the RadioLib calls Meshtastic's radio interfaces
make against RecordingHal, which logs every SPI transaction and answers with a
success status, a few scripted replies (the packet type the LoRa setters check)
and, for SX127x, a register file. One set per chip family - SX126x, SX127x,
SX128x, LR11x0, LR2021 - each ending in a list of the reply-dependent tests
still to write. SX126x and SX128x get the SPI framing their begin() would set.

RadioLib is pinned to 7.8.0 to show the failure: the LR2021 DC-DC workaround
(jgromes/RadioLib#1864) passes sizeof(uint32_t) as a word count, and under
[env:coverage] ASan aborts the LR2021 set with a stack-buffer-overflow in
readRegMem32. The other four sets pass.

* test: pin RadioLib to the DC-DC word-count fix

NomDeTom/radiolib@3509dbc8e (branch lr2021-dcdc-regmem32-len, on RadioLib
master after 7.8.0) passes 1 as the word count at the three DC-DC workaround
call sites. The overrun is gone and the LR2021 set runs to completion.

test_lr2021_dcdc_freq_lf_write_sends_one_word still fails, on a second RadioLib
bug: LRxxxx::writeCommon() sends 4 + 4*len bytes whatever the address width, so
every LR2021 WriteRegMem32 (24-bit address) carries one uninitialised trailing
byte - 10 bytes where 9 are meant.

* test: keep RecordingHal reachable across a failed assertion

A failed TEST_ASSERT longjmps out of the test, skipping destructors, so each
test's local RecordingHal leaked its transaction log. LeakSanitizer then
reported it at exit, and under [env:coverage] a plain assertion failure came
out as an ERRORED suite (run 36412456860: 26 Tests 1 Failures, then a 384-byte
leak from RecordingHal::spiTransfer). One static HAL, reset at the start of
each test, stays reachable, so a failure reports as FAILED.

* test: run the RadioLib driver suite against develop's RadioLib pin

Revert the pin to develop's 510e00cfb (RadioLib 7.7.1). That RadioLib predates
the LR2021 DC-DC workaround, so its three tests would only fail on their own
premise there. They now register only when LR2021 has the public
setRegulatorDCDC() that jgromes/RadioLib#1864 added with the workaround: any
RadioLib carrying it runs them, and none is left ignored.

Against 7.8.0 the suite shows the DC-DC overrun (ASan, run 36412379592). Against
the DC-DC fix (NomDeTom/radiolib@3509dbc8e) it shows a second bug: every LR2021
WriteRegMem32 sends one byte too many, since LRxxxx::writeCommon() still sizes
the frame for a 4-byte address after jgromes/RadioLib@e3af85867 made it 3 on
the LR2021 (run 36412456860). Neither is reachable at this pin.

* test: pin the coding-rate byte the radio interfaces send

LR11x0Interface, LR20x0Interface and SX128xInterface all call
setCodingRate(cr, cr != 7), so 4/5 goes out with the long interleaver.
The modulation tests used the driver default (short), and only counted
frames. Pass the flag the firmware passes, and add a test per chip that
reads the coding-rate field back from the last modulation-params frame:
5 for 4/5 long, 3 for 4/7 short (RadioLib 7.7.1's encoding).

Also correct test_main.cpp: at the 7.7.1 pin the LR2021 DC-DC tests are
not registered, so the set does not fail; the 7.8.0 overrun is now
stated as conditional on a RadioLib that carries it.
This commit is contained in:
Tom authored and GitHub committed 2026-09-30 12:09:08 +00:00
1 parent 778184c7a1
commit 7fe3176a40
7 files changed
+732

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
#pragma once
// A stand-in for the radio chip: records every SPI transaction and answers with scripted bytes.
// Shared by every chip family's tests in this suite; see test_main.cpp for why it is enough.
#include <RadioLib.h>
#include <algorithm>
#include <cstring>
#include <vector>
class RecordingHal : public RadioLibHal
{
public:
// A scripted answer, chosen by the leading bytes (the opcode) of what the driver sent.
struct Reply {
std::vector<uint8_t> prefix;
bool nextTransaction; // answer the transaction after the match: LRxxxx reads reply in a second one
std::vector<uint8_t> head;
uint8_t fill; // every byte after head
};
// 0x04 decodes as success on every status-byte family: LRxxxx CMD_OK, SX126x DATA_AVAILABLE,
// SX128x CMD_PROCESSED, and is neither 0x00 nor 0xFF (CHIP_NOT_FOUND).
explicit RecordingHal(uint8_t defaultFill = 0x04) : RadioLibHal(0, 1, 0, 1, 2, 3), defaultFill(defaultFill) {}
// Back to a fresh chip, keeping the allocation: see freshHal().
void reset(uint8_t fill = 0x04)
{
transactions.clear();
replies.clear();
registerEcho = false;
memset(registers, 0, sizeof(registers));
defaultFill = fill;
pending = nullptr;
nowUs = 0;
}
std::vector<std::vector<uint8_t>> transactions;
std::vector<Reply> replies;
// SX127x mode: no status byte, and RADIOLIB_SPI_PARANOID reads every register write back, so
// writes are stored and reads answered from the store.
bool registerEcho = false;
uint8_t registers[128] = {};
void reply(std::vector<uint8_t> prefix, uint8_t fill, std::vector<uint8_t> head = {}, bool nextTransaction = false)
{
replies.push_back({std::move(prefix), nextTransaction, std::move(head), fill});
}
// Transactions whose leading bytes are `prefix`.
size_t count(const std::vector<uint8_t> &prefix) const
{
size_t n = 0;
for (const auto &t : transactions)
n += startsWith(t.data(), t.size(), prefix);
return n;
}
const std::vector<uint8_t> *first(const std::vector<uint8_t> &prefix) const
{
for (const auto &t : transactions)
if (startsWith(t.data(), t.size(), prefix))
return &t;
return nullptr;
}
const std::vector<uint8_t> *last(const std::vector<uint8_t> &prefix) const
{
for (auto it = transactions.rbegin(); it != transactions.rend(); ++it)
if (startsWith(it->data(), it->size(), prefix))
return &*it;
return nullptr;
}
void pinMode(uint32_t, uint32_t) override {}
void digitalWrite(uint32_t, uint32_t) override {}
uint32_t digitalRead(uint32_t) override { return 0; } // BUSY low
void attachInterrupt(uint32_t, void (*)(void), uint32_t) override {}
void detachInterrupt(uint32_t) override {}
void delay(RadioLibTime_t ms) override { nowUs += ms * 1000; }
void delayMicroseconds(RadioLibTime_t us) override { nowUs += us; }
// Advances on every read, so a RadioLib wait loop always reaches its timeout instead of spinning.
RadioLibTime_t millis() override { return (nowUs += 1000) / 1000; }
RadioLibTime_t micros() override { return nowUs += 1000; }
long pulseIn(uint32_t, uint32_t, RadioLibTime_t) override { return 0; }
void spiBegin() override {}
void spiBeginTransaction() override {}
void spiEndTransaction() override {}
void spiEnd() override {}
void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override
{
transactions.emplace_back(out, out + len);
if (registerEcho) {
echoRegisters(out, len, in);
return;
}
const Reply *r = pending;
pending = nullptr;
for (const auto &c : replies) {
if (!startsWith(out, len, c.prefix))
continue;
if (c.nextTransaction)
pending = &c;
else if (!r)
r = &c;
}
for (size_t i = 0; i < len; i++)
in[i] = !r ? defaultFill : (i < r->head.size() ? r->head[i] : r->fill);
}
private:
uint8_t defaultFill;
const Reply *pending = nullptr;
RadioLibTime_t nowUs = 0;
static bool startsWith(const uint8_t *data, size_t len, const std::vector<uint8_t> &prefix)
{
return len >= prefix.size() && std::equal(prefix.begin(), prefix.end(), data);
}
// SX127x framing: first byte is the address, bit 7 set for a write; the rest is the burst.
void echoRegisters(const uint8_t *out, size_t len, uint8_t *in)
{
const uint8_t addr = out[0] & 0x7F;
in[0] = 0;
for (size_t i = 1; i < len; i++) {
uint8_t &reg = registers[(addr + i - 1) & 0x7F];
if (out[0] & 0x80)
reg = out[i], in[i] = 0;
else
in[i] = reg;
}
}
};
// Big-endian opcode bytes, for the 16-bit LRxxxx command set.
inline std::vector<uint8_t> op16(uint16_t opcode)
{
return {static_cast<uint8_t>(opcode >> 8), static_cast<uint8_t>(opcode & 0xFF)};
}
// A failed TEST_ASSERT longjmps out of the test, skipping destructors, so a test-local HAL would leak
// its transaction log and LeakSanitizer would turn every assertion failure into a crash. One static
// instance, reset per test, stays reachable.
inline RecordingHal &freshHal()
{
static RecordingHal hal;
hal.reset();
return hal;
}
+89
View File
@@ -0,0 +1,89 @@
#pragma once
// LR11x0 (LR1121): the calls LR11x0Interface makes, minus begin() and updateFirmware().
#include "RecordingHal.h"
#include "TestUtil.h"
// LRxxxx reads answer in a second transaction: status (0x04 = CMD_OK), then data. The LoRa setters
// ask for the packet type first; 0x02 is LoRa, which as a status byte would decode as CMD_PERR,
// so it goes in the data byte only.
#define LR11X0_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.reply(op16(RADIOLIB_LR11X0_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR11X0_PACKET_TYPE_LORA}, true); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
LR1121 radio(&mod)
static void test_lr11x0_setFrequency_sends_hertz()
{
LR11X0_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setFrequency(915.0));
const auto *t = hal.first(op16(RADIOLIB_LR11X0_CMD_SET_RF_FREQUENCY));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(6, t->size());
const uint8_t hz[] = {0x36, 0x89, 0xCA, 0xC0}; // 915000000
TEST_ASSERT_EQUAL_UINT8_ARRAY(hz, t->data() + 2, 4);
}
static void test_lr11x0_lora_modulation_setters_send_modulation_params()
{
LR11X0_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(9));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setBandwidth(250.0));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
TEST_ASSERT_EQUAL_UINT32(3, hal.count(op16(RADIOLIB_LR11X0_CMD_SET_MODULATION_PARAMS)));
}
// LR11x0Interface passes cr != 7 as the long-interleave flag: 4/7 has no long-interleaver code.
static void test_lr11x0_coding_rate_long_interleaves_except_4_7()
{
LR11X0_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
const auto *t = hal.last(op16(RADIOLIB_LR11X0_CMD_SET_MODULATION_PARAMS));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(6, t->size()); // opcode(2) + sf, bw, cr, ldro
TEST_ASSERT_EQUAL_UINT8(5, (*t)[4]); // 4/5, long interleaver
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(7, false));
t = hal.last(op16(RADIOLIB_LR11X0_CMD_SET_MODULATION_PARAMS));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT8(3, (*t)[4]); // 4/7, short interleaver
}
static void test_lr11x0_packet_setters_send_packet_params()
{
LR11X0_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setPreambleLength(16));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCRC(2));
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR11X0_CMD_SET_PACKET_PARAMS)) >= 2);
}
static void test_lr11x0_other_setters_and_modes_succeed()
{
LR11X0_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSyncWord(0x2B));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setOutputPower(22));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.standby());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startReceive());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.sleep());
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR11X0_CMD_SET_LORA_SYNC_WORD)) >= 1);
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR11X0_CMD_SET_TX_PARAMS)) >= 1);
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR11X0_CMD_SET_RX)) >= 1);
}
// Grows here - each needs replies scripted per opcode:
// begin(): getVersion() device-type check
// updateFirmware(): enter bootloader, report RADIOLIB_LR11X0_DEVICE_BOOT, then normal. Pins every
// image word written exactly once (an image that is an exact multiple of 64 words loses its last
// chunk in 7.8.0) and a failed chunk being reported (its result is currently dropped)
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): the CAD parameters sent
static void runLr11x0Tests()
{
RUN_TEST(test_lr11x0_setFrequency_sends_hertz);
RUN_TEST(test_lr11x0_lora_modulation_setters_send_modulation_params);
RUN_TEST(test_lr11x0_coding_rate_long_interleaves_except_4_7);
RUN_TEST(test_lr11x0_packet_setters_send_packet_params);
RUN_TEST(test_lr11x0_other_setters_and_modes_succeed);
}
+158
View File
@@ -0,0 +1,158 @@
#pragma once
// LR2021: the calls LR20x0Interface makes, minus begin(). The DC-DC workaround tests come first:
// under ASan the first overrun aborts the program, and it should abort on the test that names it.
#include "RecordingHal.h"
#include "TestUtil.h"
#include <modules/LR2021/LR2021_registers.h>
#include <type_traits>
#include <utility>
// jgromes/RadioLib#1864 added the DC-DC workaround together with a public setRegulatorDCDC(), so that
// method marks a RadioLib the DC-DC tests apply to. Earlier pins (7.7.1 and before) have neither.
template <typename T, typename = void> struct HasDcdcWorkaround : std::false_type {
};
template <typename T>
struct HasDcdcWorkaround<T, std::void_t<decltype(std::declval<T &>().setRegulatorDCDC())>> : std::true_type {
};
// begin() sets a frequency before anything else; without one the DC-DC workaround's closing
// setFrequency(freqMHz) fails with INVALID_FREQUENCY. The log starts after it.
static void lr2021Tune(RecordingHal &hal, LR2021 &radio)
{
(void)radio.setFrequency(915.0);
hal.transactions.clear();
}
// As LR11x0: the packet type comes back in the data byte of the next transaction. 0x00 is LoRa.
#define LR2021_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR2021_PACKET_TYPE_LORA}, true); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
LR2021 radio(&mod); \
lr2021Tune(hal, radio)
// The first transaction carrying `opcode` for register `addr`: 16-bit opcode, then a 24-bit address.
static const std::vector<uint8_t> *lr2021RegMemAccess(const RecordingHal &hal, uint16_t opcode, uint32_t addr)
{
return hal.first({static_cast<uint8_t>(opcode >> 8), static_cast<uint8_t>(opcode & 0xFF),
static_cast<uint8_t>((addr >> 16) & 0xFF), static_cast<uint8_t>((addr >> 8) & 0xFF),
static_cast<uint8_t>(addr & 0xFF)});
}
// setRxBoostedGainMode() -> setRxPath() -> setDCDCworkaround(), the route LR20x0Interface takes.
static void lr2021RunDcdcWorkaround(RecordingHal &hal, LR2021 &radio)
{
(void)hal;
(void)radio.setRxBoostedGainMode(0);
}
static void test_lr2021_setRxPath_runs_the_dcdc_workaround()
{
LR2021_RADIO(hal);
lr2021RunDcdcWorkaround(hal, radio);
// Guards the premise: without these the two word-count tests below would pass vacuously.
TEST_ASSERT_NOT_NULL(lr2021RegMemAccess(hal, RADIOLIB_LR2021_CMD_READ_REG_MEM_32, RADIOLIB_LR2021_REG_DCDC_ADC_CTRL));
TEST_ASSERT_NOT_NULL(lr2021RegMemAccess(hal, RADIOLIB_LR2021_CMD_WRITE_REG_MEM_32, RADIOLIB_LR2021_REG_DCDC_FREQ_LF));
}
static void test_lr2021_dcdc_adc_ctrl_read_asks_for_one_word()
{
LR2021_RADIO(hal);
lr2021RunDcdcWorkaround(hal, radio);
const auto *req = lr2021RegMemAccess(hal, RADIOLIB_LR2021_CMD_READ_REG_MEM_32, RADIOLIB_LR2021_REG_DCDC_ADC_CTRL);
TEST_ASSERT_NOT_NULL(req);
// READ_REG_MEM_32 request: opcode(2) + address(3) + word count(1)
TEST_ASSERT_EQUAL_UINT32(6, req->size());
TEST_ASSERT_EQUAL_UINT8(1, (*req)[5]);
}
static void test_lr2021_dcdc_freq_lf_write_sends_one_word()
{
LR2021_RADIO(hal);
lr2021RunDcdcWorkaround(hal, radio);
const auto *wr = lr2021RegMemAccess(hal, RADIOLIB_LR2021_CMD_WRITE_REG_MEM_32, RADIOLIB_LR2021_REG_DCDC_FREQ_LF);
TEST_ASSERT_NOT_NULL(wr);
// WRITE_REG_MEM_32: opcode(2) + address(3) + one data word(4); four words is 21 bytes
TEST_ASSERT_EQUAL_UINT32(2 + 3 + 4, wr->size());
}
static void test_lr2021_setFrequency_sends_hertz()
{
LR2021_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setFrequency(915.0));
const auto *t = hal.first(op16(RADIOLIB_LR2021_CMD_SET_RF_FREQUENCY));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(6, t->size());
const uint8_t hz[] = {0x36, 0x89, 0xCA, 0xC0}; // 915000000
TEST_ASSERT_EQUAL_UINT8_ARRAY(hz, t->data() + 2, 4);
}
static void test_lr2021_lora_modulation_setters_send_modulation_params()
{
LR2021_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(9));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setBandwidth(250.0));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
TEST_ASSERT_EQUAL_UINT32(3, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_MODULATION_PARAMS)));
}
// LR20x0Interface passes cr != 7 as the long-interleave flag: 4/7 has no long-interleaver code.
static void test_lr2021_coding_rate_long_interleaves_except_4_7()
{
LR2021_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
const auto *t = hal.last(op16(RADIOLIB_LR2021_CMD_SET_LORA_MODULATION_PARAMS));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(4, t->size()); // opcode(2) + sf|bw, cr|ldro
TEST_ASSERT_EQUAL_UINT8(5, (*t)[3] >> 4); // 4/5, long interleaver
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(7, false));
t = hal.last(op16(RADIOLIB_LR2021_CMD_SET_LORA_MODULATION_PARAMS));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT8(3, (*t)[3] >> 4); // 4/7, short interleaver
}
static void test_lr2021_packet_setters_send_packet_params()
{
LR2021_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setPreambleLength(16));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCRC(2));
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_PACKET_PARAMS)) >= 2);
}
static void test_lr2021_other_setters_and_modes_succeed()
{
LR2021_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSyncWord(0x2B));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setOutputPower(22));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.standby());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startReceive());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.sleep());
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_SYNCWORD)) >= 1);
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR2021_CMD_SET_TX_PARAMS)) >= 1);
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR2021_CMD_SET_RX)) >= 1);
}
// Grows here - each needs replies scripted per opcode:
// begin(): getVersion() check and the calibration sequence
// resetDCDCworkaround() via setPacketType(): only begin() reaches it; same one-word write as above
// setPaTable(): custom LF table entry chosen per output power (cf. meshtastic/firmware#11980)
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): the CAD parameters sent (LR2021 has two CAD commands)
static void runLr2021Tests()
{
if constexpr (HasDcdcWorkaround<LR2021>::value) {
RUN_TEST(test_lr2021_setRxPath_runs_the_dcdc_workaround);
RUN_TEST(test_lr2021_dcdc_adc_ctrl_read_asks_for_one_word);
RUN_TEST(test_lr2021_dcdc_freq_lf_write_sends_one_word);
}
RUN_TEST(test_lr2021_setFrequency_sends_hertz);
RUN_TEST(test_lr2021_lora_modulation_setters_send_modulation_params);
RUN_TEST(test_lr2021_coding_rate_long_interleaves_except_4_7);
RUN_TEST(test_lr2021_packet_setters_send_packet_params);
RUN_TEST(test_lr2021_other_setters_and_modes_succeed);
}
+114
View File
@@ -0,0 +1,114 @@
#pragma once
// SX126x (SX1262): the calls SX126xInterface makes, minus begin().
#include "RecordingHal.h"
#include "TestUtil.h"
// The LoRa setters ask the chip for its packet type first. 0x01 is LoRa, and as a status byte it
// matches no SX126x error code, so the whole reply can be 0x01.
static void sx126xAnswerLora(RecordingHal &hal)
{
hal.reply({RADIOLIB_SX126X_CMD_GET_PACKET_TYPE}, RADIOLIB_SX126X_PACKET_TYPE_LORA);
}
// Exposes RadioLib's own status parser, which is protected in SX126x.
struct TestSX1262 : public SX1262 {
using SX1262::SX1262;
static int16_t parseStatus(uint8_t in) { return SPIparseStatus(in); }
};
// SX126x sets its SPI framing in begin() (modSetup()), not its constructor, and begin() cannot run
// without a chip. This is the same framing, as of RadioLib 7.8.0.
static void sx126xFraming(Module &mod)
{
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_ADDR] = Module::BITS_16;
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_CMD] = Module::BITS_8;
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] = Module::BITS_8;
mod.spiConfig.statusPos = 1;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_READ] = RADIOLIB_SX126X_CMD_READ_REGISTER;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_WRITE] = RADIOLIB_SX126X_CMD_WRITE_REGISTER;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_NOP] = RADIOLIB_SX126X_CMD_NOP;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_STATUS] = RADIOLIB_SX126X_CMD_GET_STATUS;
mod.spiConfig.stream = true;
mod.spiConfig.parseStatusCb = TestSX1262::parseStatus;
}
#define SX126X_RADIO(hal) \
RecordingHal &hal = freshHal(); \
sx126xAnswerLora(hal); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
TestSX1262 radio(&mod); \
sx126xFraming(mod)
static void test_sx126x_setFrequency_sends_the_frf_word()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setFrequency(915.0));
// frf = 915 MHz * 2^25 / 32 MHz = 0x39300000
const auto *t = hal.first({RADIOLIB_SX126X_CMD_SET_RF_FREQUENCY});
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(5, t->size());
const uint8_t frf[] = {0x39, 0x30, 0x00, 0x00};
TEST_ASSERT_EQUAL_UINT8_ARRAY(frf, t->data() + 1, 4);
}
static void test_sx126x_lora_modulation_setters_send_modulation_params()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(9));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setBandwidth(250.0));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5));
TEST_ASSERT_EQUAL_UINT32(3, hal.count({RADIOLIB_SX126X_CMD_SET_MODULATION_PARAMS}));
}
static void test_sx126x_packet_setters_send_packet_params()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setPreambleLength(16));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCRC(2));
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS}) >= 2);
}
static void test_sx126x_setSyncWord_writes_a_register()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSyncWord(0x2B));
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_WRITE_REGISTER}) >= 1);
}
static void test_sx126x_setOutputPower_sends_pa_config_and_tx_params()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setOutputPower(22));
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_PA_CONFIG}) >= 1);
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_TX_PARAMS}) >= 1);
}
static void test_sx126x_mode_commands_reach_the_chip()
{
SX126X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.standby());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startReceive());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.sleep());
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_STANDBY}) >= 1);
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_RX}) >= 1);
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX126X_CMD_SET_SLEEP}) >= 1);
}
// Grows here - each needs replies scripted per opcode, not just a status byte:
// begin(): version string check (SX126X_REG_VERSION_STRING)
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): the CAD parameters sent, per the part's symNum encoding
// startReceiveDutyCycleAuto(): the RX and sleep periods derived from preamble length
static void runSx126xTests()
{
RUN_TEST(test_sx126x_setFrequency_sends_the_frf_word);
RUN_TEST(test_sx126x_lora_modulation_setters_send_modulation_params);
RUN_TEST(test_sx126x_packet_setters_send_packet_params);
RUN_TEST(test_sx126x_setSyncWord_writes_a_register);
RUN_TEST(test_sx126x_setOutputPower_sends_pa_config_and_tx_params);
RUN_TEST(test_sx126x_mode_commands_reach_the_chip);
}
+73
View File
@@ -0,0 +1,73 @@
#pragma once
// SX127x (SX1276): the calls RF95Interface makes, minus begin(). Register-mapped, so the HAL runs in
// echo mode and the assertions read the resulting register values.
#include "RecordingHal.h"
#include "TestUtil.h"
// The setters check REG_OP_MODE bit 7 for LoRa mode; begin() would have set it.
#define SX127X_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.registerEcho = true; \
hal.registers[RADIOLIB_SX127X_REG_OP_MODE] = RADIOLIB_SX127X_LORA | RADIOLIB_SX127X_STANDBY; \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC); \
SX1276 radio(&mod)
static void test_sx127x_setFrequency_writes_the_frf_registers()
{
SX127X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setFrequency(915.0));
// frf = 915 MHz * 2^19 / 32 MHz = 0xE4C000
TEST_ASSERT_EQUAL_HEX8(0xE4, hal.registers[RADIOLIB_SX127X_REG_FRF_MSB]);
TEST_ASSERT_EQUAL_HEX8(0xC0, hal.registers[RADIOLIB_SX127X_REG_FRF_MID]);
TEST_ASSERT_EQUAL_HEX8(0x00, hal.registers[RADIOLIB_SX127X_REG_FRF_LSB]);
}
static void test_sx127x_setSpreadingFactor_writes_modem_config_2()
{
SX127X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(9));
TEST_ASSERT_EQUAL_HEX8(9, hal.registers[RADIOLIB_SX127X_REG_MODEM_CONFIG_2] >> 4);
}
static void test_sx127x_setSyncWord_writes_the_sync_register()
{
SX127X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSyncWord(0x2B));
TEST_ASSERT_EQUAL_HEX8(0x2B, hal.registers[RADIOLIB_SX127X_REG_SYNC_WORD]);
}
static void test_sx127x_setPreambleLength_writes_both_bytes()
{
SX127X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setPreambleLength(16));
TEST_ASSERT_EQUAL_HEX8(0x00, hal.registers[RADIOLIB_SX127X_REG_PREAMBLE_MSB]);
TEST_ASSERT_EQUAL_HEX8(16, hal.registers[RADIOLIB_SX127X_REG_PREAMBLE_LSB]);
}
static void test_sx127x_other_setters_and_modes_succeed()
{
SX127X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setBandwidth(250.0));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setOutputPower(17));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.standby());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startReceive());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.sleep());
}
// Grows here - each needs more than an echoing register file:
// begin(): REG_VERSION check and the reset sequence
// readData() / getPacketLength(): REG_RX_NB_BYTES larger than the caller's buffer
// getRSSI() / getSNR(): decoding of REG_PKT_RSSI_VALUE / REG_PKT_SNR_VALUE
// scanChannel(): CAD done/detected flags in REG_IRQ_FLAGS
static void runSx127xTests()
{
RUN_TEST(test_sx127x_setFrequency_writes_the_frf_registers);
RUN_TEST(test_sx127x_setSpreadingFactor_writes_modem_config_2);
RUN_TEST(test_sx127x_setSyncWord_writes_the_sync_register);
RUN_TEST(test_sx127x_setPreambleLength_writes_both_bytes);
RUN_TEST(test_sx127x_other_setters_and_modes_succeed);
}
+99
View File
@@ -0,0 +1,99 @@
#pragma once
// SX128x (SX1280): the calls SX128xInterface makes, minus begin().
#include "RecordingHal.h"
#include "TestUtil.h"
// SX128x sets its SPI framing in begin() (modSetup()), not its constructor, and begin() cannot run
// without a chip. This is the same framing, as of RadioLib 7.8.0, minus the status parser: it is
// private in SX128x, and the HAL never answers with an error status anyway.
static void sx128xFraming(Module &mod)
{
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_ADDR] = Module::BITS_16;
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_CMD] = Module::BITS_8;
mod.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] = Module::BITS_8;
mod.spiConfig.statusPos = 0;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_READ] = RADIOLIB_SX128X_CMD_READ_REGISTER;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_WRITE] = RADIOLIB_SX128X_CMD_WRITE_REGISTER;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_NOP] = RADIOLIB_SX128X_CMD_NOP;
mod.spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_STATUS] = RADIOLIB_SX128X_CMD_GET_STATUS;
mod.spiConfig.stream = true;
mod.spiConfig.parseStatusCb = nullptr;
}
// The LoRa setters ask for the packet type first. 0x01 is LoRa and matches no SX128x error code.
#define SX128X_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.reply({RADIOLIB_SX128X_CMD_GET_PACKET_TYPE}, RADIOLIB_SX128X_PACKET_TYPE_LORA); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
SX1280 radio(&mod); \
sx128xFraming(mod)
static void test_sx128x_setFrequency_sends_rf_frequency()
{
SX128X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setFrequency(2440.0));
const auto *t = hal.first({RADIOLIB_SX128X_CMD_SET_RF_FREQUENCY});
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(4, t->size()); // opcode + 24-bit frf
}
static void test_sx128x_lora_modulation_setters_send_modulation_params()
{
SX128X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(9));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setBandwidth(812.5));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
TEST_ASSERT_EQUAL_UINT32(3, hal.count({RADIOLIB_SX128X_CMD_SET_MODULATION_PARAMS}));
}
// SX128xInterface passes cr != 7 as the long-interleave flag: 4/7 has no long-interleaver code.
static void test_sx128x_coding_rate_long_interleaves_except_4_7()
{
SX128X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(5, true));
const auto *t = hal.last({RADIOLIB_SX128X_CMD_SET_MODULATION_PARAMS});
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(4, t->size()); // opcode + sf, bw, cr
TEST_ASSERT_EQUAL_UINT8(5, (*t)[3]); // 4/5, long interleaver
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCodingRate(7, false));
t = hal.last({RADIOLIB_SX128X_CMD_SET_MODULATION_PARAMS});
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT8(3, (*t)[3]); // 4/7, short interleaver
}
static void test_sx128x_packet_setters_send_packet_params()
{
SX128X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setPreambleLength(16));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setCRC(2));
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX128X_CMD_SET_PACKET_PARAMS}) >= 2);
}
static void test_sx128x_other_setters_and_modes_succeed()
{
SX128X_RADIO(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSyncWord(0x12));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setOutputPower(10));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.standby());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startReceive());
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.sleep());
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX128X_CMD_SET_TX_PARAMS}) >= 1);
TEST_ASSERT_TRUE(hal.count({RADIOLIB_SX128X_CMD_SET_RX}) >= 1);
}
// Grows here - each needs replies scripted per opcode:
// begin(): version string check
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): CAD parameters and the CAD-done IRQ
static void runSx128xTests()
{
RUN_TEST(test_sx128x_setFrequency_sends_rf_frequency);
RUN_TEST(test_sx128x_lora_modulation_setters_send_modulation_params);
RUN_TEST(test_sx128x_coding_rate_long_interleaves_except_4_7);
RUN_TEST(test_sx128x_packet_setters_send_packet_params);
RUN_TEST(test_sx128x_other_setters_and_modes_succeed);
}
+46
View File
@@ -0,0 +1,46 @@
// Unit tests for the RadioLib driver calls Meshtastic's radio interfaces make, one set per chip
// family: SX126x (sx126x_tests.h), SX127x (sx127x_tests.h), SX128x (sx128x_tests.h), LR11x0
// (lr11x0_tests.h) and LR2021 (lr2021_tests.h), against the RadioLib pinned in platformio.ini.
//
// No chip is needed. RecordingHal (RecordingHal.h) logs every SPI transaction and answers with a
// status byte every status-byte family reads as success, plus the few scripted replies the setters
// check before acting (the packet type); for SX127x it keeps a register file instead. That reaches
// every setter and mode command the interfaces use, and the tests pin what reaches the chip: the
// opcode, the frame length, and where the value is fixed, the payload.
//
// The regressions guarded are driver changes that send the wrong bytes, or read or write past a
// buffer, on paths no hardware-free test reached before. Under [env:coverage] (-fsanitize=address)
// an out-of-bounds access aborts the program at the call. The first such bug is jgromes/RadioLib#1864:
// the LR2021 DC-DC workaround passes sizeof(uint32_t) as a word count, overrunning the stack on
// every setRxPath() and LoRa modulation change. The pin here, 7.7.1, predates that workaround, so
// the LR2021 DC-DC tests register only on a RadioLib that has it (see lr2021_tests.h). On one that
// also carries the overrun (7.8.0) they abort, so the LR2021 set runs last and the rest report first.
//
// Anything that decodes a chip reply (begin(), readData(), getRSSI(), updateFirmware()) needs replies
// scripted per opcode; each family's header lists those under "Grows here".
#include "TestUtil.h"
#include <RadioLib.h>
#include <unity.h>
#include "lr11x0_tests.h"
#include "lr2021_tests.h"
#include "sx126x_tests.h"
#include "sx127x_tests.h"
#include "sx128x_tests.h"
void setUp(void) {}
void tearDown(void) {}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
runSx126xTests();
runSx127xTests();
runSx128xTests();
runLr11x0Tests();
runLr2021Tests(); // last: its DC-DC tests abort under ASan on a RadioLib with the #1864 overrun
exit(UNITY_END());
}
void loop() {}