mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 19:38:20 +00:00
x1 debug v1
This commit is contained in:
@@ -137,3 +137,6 @@ MESHTIMESYNC_PLAN.md
|
||||
releasenotes/RELEASE_NOTES_1.16.3-zephcore.md
|
||||
releasenotes/RELEASE_NOTES_1.16.4-zephcore.md
|
||||
handover_thinknode_m9.md
|
||||
|
||||
# Vendor reference docs (datasheets, errata). Local only — not ours to redistribute.
|
||||
/datasheets/
|
||||
|
||||
@@ -168,6 +168,12 @@ BOARDS = [
|
||||
# --- nRF52: new ZephCore-only hardware (own tile) ---------------------
|
||||
dict(stem="lilygo_timpulse_plus", kind="nrf", device="LilyGo T-Impulse Plus", new=True, img="lora.svg"),
|
||||
dict(stem="heltec_t096", kind="nrf", device="Heltec Mesh Node T096", new=True, img="lora.svg"),
|
||||
# Name is pre-matched to MeshCore's flasher convention ("Seeed Studio" +
|
||||
# full product name, as with "Seeed Studio SenseCAP T1000-E"), so this
|
||||
# folds into their tile automatically the moment they add one -- the merge
|
||||
# is by exact name. Their flasher config has no X1 entry yet even though
|
||||
# MeshCore firmware supports it, hence new=True and our own photo for now.
|
||||
# WHEN IT APPEARS: drop new=True and own_img so their art and tile win.
|
||||
dict(stem="meshtracker_x1", kind="nrf", device="Seeed Studio SenseCAP MeshTracker X1",
|
||||
new=True, own_img="meshtracker_x1.jpg"),
|
||||
|
||||
|
||||
@@ -1710,36 +1710,30 @@ static void gps_uart_dump_hw_state(void)
|
||||
*/
|
||||
static void gps_dump_gpio_states(void)
|
||||
{
|
||||
/* Port/pin come from the gpio_dt_spec so the board's real wiring is
|
||||
* printed. These used to be hardcoded T1000-E pin numbers, which read
|
||||
* as plausible nonsense on every other board. */
|
||||
#define GPS_LOG_PIN(_label, _spec) if (gpio_is_ready_dt(&(_spec))) { LOG_INF(" %-14s %s.%02u: %d", _label, (_spec).port->name, (_spec).pin, gpio_pin_get_dt(&(_spec))); }
|
||||
|
||||
LOG_INF("GPS GPIO states after power-up:");
|
||||
if (gpio_is_ready_dt(&gps_enable_gpio)) {
|
||||
LOG_INF(" GPS_EN (P1.11): %d", gpio_pin_get_dt(&gps_enable_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_EN", gps_enable_gpio);
|
||||
#if HAS_GPS_VRTC
|
||||
if (gpio_is_ready_dt(&gps_vrtc_gpio)) {
|
||||
LOG_INF(" GPS_VRTC_EN (P0.08): %d", gpio_pin_get_dt(&gps_vrtc_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_VRTC_EN", gps_vrtc_gpio);
|
||||
#endif
|
||||
#if HAS_GPS_RESET
|
||||
if (gpio_is_ready_dt(&gps_reset_gpio)) {
|
||||
LOG_INF(" GPS_RESET (P1.15): %d", gpio_pin_get_dt(&gps_reset_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_RESET", gps_reset_gpio);
|
||||
#endif
|
||||
#if HAS_GPS_SLEEP
|
||||
if (gpio_is_ready_dt(&gps_sleep_gpio)) {
|
||||
LOG_INF(" GPS_SLEEP_INT (P1.12): %d", gpio_pin_get_dt(&gps_sleep_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_SLEEP_INT", gps_sleep_gpio);
|
||||
#endif
|
||||
#if HAS_GPS_RTCINT
|
||||
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
|
||||
LOG_INF(" GPS_RTC_INT (P0.15): %d", gpio_pin_get_dt(&gps_rtcint_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_RTC_INT", gps_rtcint_gpio);
|
||||
#endif
|
||||
#if HAS_GPS_RESETB
|
||||
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
|
||||
LOG_INF(" GPS_RESETB (P1.14): %d (INPUT_PULLUP, expect 1)",
|
||||
gpio_pin_get_dt(&gps_resetb_gpio));
|
||||
}
|
||||
GPS_LOG_PIN("GPS_RESETB", gps_resetb_gpio); /* INPUT_PULLUP, expect 1 */
|
||||
#endif
|
||||
|
||||
#undef GPS_LOG_PIN
|
||||
}
|
||||
#endif /* HAS_GPS_POWER_CONTROL */
|
||||
|
||||
|
||||
@@ -76,12 +76,21 @@ static lr20xx_hal_status_t wait_on_busy(struct lr20xx_hal_context *ctx)
|
||||
static lr20xx_hal_status_t check_device_ready(struct lr20xx_hal_context *ctx)
|
||||
{
|
||||
if (!ctx->radio_is_sleeping) {
|
||||
return wait_on_busy(ctx);
|
||||
/* In RX duty cycle the chip sleeps between windows on its own, so
|
||||
* the flag above is false while BUSY is high and the radio is not
|
||||
* listening. Waiting would just burn the BUSY timeout; wake it the
|
||||
* same way an explicit sleep is woken. */
|
||||
if (!ctx->auto_sleeps || !gpio_pin_get_dt(&ctx->busy)) {
|
||||
return wait_on_busy(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wake from sleep: NSS pulse ≥10us per LR2021 datasheet §5.4.2 */
|
||||
/* Wake from sleep: the chip leaves Sleep when NSS is held low for 100us
|
||||
* (datasheet, Sleep mode). Semtech's reference HAL allows 1 ms; keep that
|
||||
* margin — a short pulse leaves the radio asleep and every following
|
||||
* command is answered by a chip that is not listening. */
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
k_busy_wait(10); /* ≥10us NSS hold; k_busy_wait unit is microseconds */
|
||||
k_busy_wait(1000);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
|
||||
ctx->radio_is_sleeping = false;
|
||||
@@ -232,31 +241,110 @@ lr20xx_hal_status_t lr20xx_hal_write(const void *context, const uint8_t *command
|
||||
* so it surfaces as nonsense lengths and all-zero status reads rather than as
|
||||
* an SPI error. Keep every read in one transaction.
|
||||
*
|
||||
* NULL buffers clock 0x00 (the LR2021 NOP opcode) on MOSI and discard on MISO,
|
||||
* so no scratch buffer is needed regardless of length.
|
||||
* NULL TX buffers clock the SPI controller's over-read character, NOT zero --
|
||||
* Nordic defaults it to 0xff, which the LR2021 reads as a bogus opcode and
|
||||
* rejects. Boards using this driver must set overrun-character = <0x00> (the
|
||||
* LR2021 NOP) on the SPI node.
|
||||
*/
|
||||
static int lr20xx_spi_read_frame(struct lr20xx_hal_context *ctx, const uint8_t *command,
|
||||
uint16_t command_length, uint8_t *data, uint16_t data_length)
|
||||
{
|
||||
const struct spi_buf tx_bufs[] = {
|
||||
{ .buf = (uint8_t *)command, .len = command_length },
|
||||
{ .buf = NULL, .len = LR20XX_STAT_LEN + data_length },
|
||||
};
|
||||
const struct spi_buf rx_bufs[] = {
|
||||
{ .buf = NULL, .len = command_length + LR20XX_STAT_LEN },
|
||||
{ .buf = data, .len = data_length },
|
||||
};
|
||||
const struct spi_buf_set tx = { .buffers = tx_bufs, .count = 2 };
|
||||
const struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
|
||||
int ret;
|
||||
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
ret = spi_transceive(ctx->spi_dev, &ctx->spi_cfg, &tx, &rx);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
/* Phase 1: the command, in its own NSS window. */
|
||||
{
|
||||
const struct spi_buf tx_buf = {
|
||||
.buf = (uint8_t *)command,
|
||||
.len = command_length,
|
||||
};
|
||||
const struct spi_buf_set tx = { .buffers = &tx_buf, .count = 1 };
|
||||
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
ret = spi_write(ctx->spi_dev, &ctx->spi_cfg, &tx);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/* The answer is not ready until BUSY drops — without this the second
|
||||
* window clocks out whatever the chip had, two bytes early. */
|
||||
if (wait_on_busy(ctx) != LR20XX_HAL_STATUS_OK) {
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
|
||||
/* Phase 2: two dummy bytes absorb the stat header, then the payload.
|
||||
* The stat header lands in a real buffer rather than being discarded
|
||||
* with a NULL one: spi_nrfx_spim rejects a transfer whose first TX and
|
||||
* RX buffers are both NULL (-EINVAL), and TX is legitimately NULL here. */
|
||||
{
|
||||
uint8_t stat[LR20XX_STAT_LEN];
|
||||
const struct spi_buf tx_buf = {
|
||||
.buf = NULL,
|
||||
.len = LR20XX_STAT_LEN + data_length,
|
||||
};
|
||||
const struct spi_buf rx_bufs[] = {
|
||||
{ .buf = stat, .len = LR20XX_STAT_LEN },
|
||||
{ .buf = data, .len = data_length },
|
||||
};
|
||||
const struct spi_buf_set tx = { .buffers = &tx_buf, .count = 1 };
|
||||
const struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
|
||||
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
ret = spi_transceive(ctx->spi_dev, &ctx->spi_cfg, &tx, &rx);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_LOG)
|
||||
/*
|
||||
* Dump one command frame from byte 0 — command echo, stat header and payload
|
||||
* all in one line, nothing discarded. The single-NSS read layout above has
|
||||
* never been checked against real silicon; this shows where the payload
|
||||
* actually starts instead of assuming it.
|
||||
*/
|
||||
void lr20xx_hal_debug_raw_frame(const void *context, const uint8_t *command,
|
||||
uint16_t command_length, uint16_t extra)
|
||||
{
|
||||
struct lr20xx_hal_context *ctx = (struct lr20xx_hal_context *)context;
|
||||
uint8_t rx[24] = { 0 };
|
||||
uint16_t total = command_length + extra;
|
||||
|
||||
if (total > sizeof(rx)) {
|
||||
total = sizeof(rx);
|
||||
}
|
||||
|
||||
const struct spi_buf tx_bufs[] = {
|
||||
{ .buf = (uint8_t *)command, .len = command_length },
|
||||
{ .buf = NULL, .len = (size_t)(total - command_length) },
|
||||
};
|
||||
const struct spi_buf rx_bufs[] = {
|
||||
{ .buf = rx, .len = total },
|
||||
};
|
||||
const struct spi_buf_set tx = { .buffers = tx_bufs, .count = 2 };
|
||||
const struct spi_buf_set rxs = { .buffers = rx_bufs, .count = 1 };
|
||||
|
||||
if (check_device_ready(ctx) != LR20XX_HAL_STATUS_OK) {
|
||||
LOG_WRN("raw frame: device not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
int ret = spi_transceive(ctx->spi_dev, &ctx->spi_cfg, &tx, &rxs);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
|
||||
if (ret < 0) {
|
||||
LOG_ERR("raw frame: spi err %d", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_HEXDUMP_INF(rx, total, "frame A: one NSS window, from byte 0");
|
||||
}
|
||||
#endif /* CONFIG_LOG */
|
||||
|
||||
lr20xx_hal_status_t lr20xx_hal_read(const void *context, const uint8_t *command,
|
||||
const uint16_t command_length,
|
||||
uint8_t *data, const uint16_t data_length)
|
||||
@@ -342,11 +430,28 @@ lr20xx_hal_status_t lr20xx_hal_direct_read_fifo(const void *context,
|
||||
return LR20XX_HAL_STATUS_ERROR;
|
||||
}
|
||||
|
||||
/* The RX FIFO answers with the same [stat header][payload] framing as
|
||||
* every other read — it is not a raw byte stream. Skipping the header
|
||||
* here is what keeps the returned packet aligned; consuming it as
|
||||
* payload shifts the whole frame two bytes and corrupts every receive. */
|
||||
ret = lr20xx_spi_read_frame(ctx, command, command_length, data, data_length);
|
||||
/* Unlike every other read, the FIFO answers inside the command's own NSS
|
||||
* window with no stat header and no BUSY wait — command bytes out, payload
|
||||
* straight back. Semtech's reference HAL is explicit about this
|
||||
* (lr20xx_hal_direct_read_fifo); treating it like a normal read releases
|
||||
* NSS mid-frame and eats two payload bytes as a header. */
|
||||
{
|
||||
const struct spi_buf tx_bufs[] = {
|
||||
{ .buf = (uint8_t *)command, .len = command_length },
|
||||
{ .buf = NULL, .len = data_length },
|
||||
};
|
||||
const struct spi_buf rx_bufs[] = {
|
||||
{ .buf = NULL, .len = command_length },
|
||||
{ .buf = data, .len = data_length },
|
||||
};
|
||||
const struct spi_buf_set tx = { .buffers = tx_bufs, .count = 2 };
|
||||
const struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
|
||||
|
||||
gpio_pin_set_dt(&ctx->nss, 1);
|
||||
ret = spi_transceive(ctx->spi_dev, &ctx->spi_cfg, &tx, &rx);
|
||||
gpio_pin_set_dt(&ctx->nss, 0);
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
LOG_ERR("SPI FIFO read failed: %d", ret);
|
||||
return LR20XX_HAL_STATUS_ERROR;
|
||||
|
||||
@@ -36,6 +36,10 @@ struct lr20xx_hal_context {
|
||||
struct gpio_dt_spec dio1; /* DIO1 interrupt */
|
||||
|
||||
volatile bool radio_is_sleeping;
|
||||
/* Set while RX duty cycling: the chip parks itself in sleep between
|
||||
* windows without the host ever sending SetSleep, so radio_is_sleeping
|
||||
* cannot be trusted to spot it. */
|
||||
volatile bool auto_sleeps;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -63,6 +67,12 @@ void lr20xx_hal_enable_dio1_irq(struct lr20xx_hal_context *ctx);
|
||||
/** @brief Disable DIO1 interrupt */
|
||||
void lr20xx_hal_disable_dio1_irq(struct lr20xx_hal_context *ctx);
|
||||
|
||||
#if IS_ENABLED(CONFIG_LOG)
|
||||
/** @brief Log a whole command frame from byte 0, nothing discarded. */
|
||||
void lr20xx_hal_debug_raw_frame(const void *context, const uint8_t *command,
|
||||
uint16_t command_length, uint16_t extra);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -196,12 +196,9 @@ lr20xx_status_t lr20xx_radio_common_calibrate_front_end_helper(
|
||||
const uint32_t freq_hz = front_end_calibration_structures[front_end_calibration_value_index].frequency_in_hertz;
|
||||
const lr20xx_radio_common_rx_path_t rx_path =
|
||||
front_end_calibration_structures[front_end_calibration_value_index].rx_path;
|
||||
/* round(freq_hz / 4MHz): calibrate the NEAREST 4MHz bin — this is the
|
||||
* bin the chip selects internally for set_rf_freq, so it must match or
|
||||
* RX raises RXFREQ_NO_FE_CAL (0x0200) and TX is refused (PERR).
|
||||
* RadioLib uses round-to-nearest; ceil() picked the wrong bin. */
|
||||
// Perform a ceil() to get a value for freq_4mhz corresponding to a frequency higher than or equal to freq_hz
|
||||
const uint16_t freq_4mhz =
|
||||
( uint16_t ) ( ( freq_hz + ( LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ / 2u ) ) /
|
||||
( uint16_t ) ( ( freq_hz + LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ - 1u ) /
|
||||
LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ );
|
||||
raw_calibration_values[front_end_calibration_value_index] =
|
||||
( uint16_t ) ( ( ( rx_path == LR20XX_RADIO_COMMON_RX_PATH_HF ) ? 0x8000u : 0x0000u ) | freq_4mhz );
|
||||
|
||||
@@ -21,3 +21,9 @@ CONFIG_ZEPHCORE_RADIO_LR2021=y
|
||||
# DRV2605L vibration motor
|
||||
CONFIG_HAPTICS=y
|
||||
CONFIG_HAPTICS_DRV2605=y
|
||||
|
||||
# The flash sits behind a power switch on P0.15, held on by a gpio-hog.
|
||||
# Hogs run at POST_KERNEL 41 — the same priority as the QSPI NOR driver's
|
||||
# default, so which one wins is link order. Probing an unpowered flash fails
|
||||
# the JEDEC check and /ext never mounts. Move the flash after the hog.
|
||||
CONFIG_NORDIC_QSPI_NOR_INIT_PRIORITY=45
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# MeshTracker X1 bring-up logging.
|
||||
#
|
||||
# Everything needed to see why a peripheral did not come up, in one build:
|
||||
# west build -b meshtracker_x1 zephcore --pristine -- \
|
||||
# -DEXTRA_CONF_FILE="boards/common/debug.conf;boards/nrf52840/meshtracker_x1/bringup.conf"
|
||||
#
|
||||
# Not for daily use — this is loud and costs RAM.
|
||||
|
||||
# LR2021: chip dumps, modem_cfg lines, decoded error words, raw SPI frames
|
||||
CONFIG_LORA_LOG_LEVEL_DBG=y
|
||||
|
||||
# QSPI NOR: prints the JEDEC id it actually read when the probe fails
|
||||
CONFIG_FLASH_LOG_LEVEL_DBG=y
|
||||
|
||||
# DRV2605: the driver logs its init failures at debug level only
|
||||
CONFIG_HAPTICS_LOG_LEVEL_DBG=y
|
||||
|
||||
# SPA06 + the RTC probe + our own sensor manager
|
||||
CONFIG_SENSOR_LOG_LEVEL_DBG=y
|
||||
CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL_DBG=y
|
||||
|
||||
# Why a mount failed, not just that it did
|
||||
CONFIG_FS_LOG_LEVEL_DBG=y
|
||||
CONFIG_I2C_LOG_LEVEL_INF=y
|
||||
|
||||
# The default 8192 buffer plus the extra modules does not fit alongside the
|
||||
# LR2021 debug output on this board — messages get dropped either way, and a
|
||||
# smaller buffer leaves room for the modules themselves.
|
||||
CONFIG_LOG_BUFFER_SIZE=4096
|
||||
@@ -60,8 +60,13 @@
|
||||
compatible = "gpio-keys";
|
||||
|
||||
user_button: button_0 {
|
||||
/* Active HIGH (pressed = VCC), external pull-down on board */
|
||||
gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
|
||||
/* Active HIGH (pressed = VCC), external pull-down on board.
|
||||
* The internal pull-down is declared as well so System OFF
|
||||
* arms SENSE with PULLDOWN rather than NOPULL: the teardown
|
||||
* cuts the sensor rail before configuring SENSE, and a
|
||||
* floating pin with SENSE_HIGH asserts DETECT immediately,
|
||||
* which exits System OFF as fast as it is entered. */
|
||||
gpios = <&gpio0 6 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
|
||||
zephyr,code = <INPUT_KEY_0>;
|
||||
label = "User Button";
|
||||
};
|
||||
@@ -330,6 +335,12 @@
|
||||
compatible = "nordic,nrf-spim";
|
||||
status = "okay";
|
||||
cs-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
|
||||
/* Clock 0x00 on MOSI when there is nothing to send. Nordic defaults the
|
||||
* over-read character to 0xff, and the LR2021 parses those bytes: a bare
|
||||
* status read then looks like opcode 0xffff, which the chip rejects and
|
||||
* flags as a command error. 0x00 is the LR2021 NOP, which is what both
|
||||
* Semtech's reference HAL and RadioLib send. */
|
||||
overrun-character = <0x00>;
|
||||
pinctrl-0 = <&spi2_default>;
|
||||
pinctrl-1 = <&spi2_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
@@ -129,6 +129,8 @@
|
||||
pinctrl-1 = <&spi2_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
cs-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>; /* P1.13 LR2021_CS */
|
||||
/* 0x00 = LR2021 NOP; Nordic's 0xff default reads as a bogus opcode */
|
||||
overrun-character = <0x00>;
|
||||
|
||||
lora: lora@0 {
|
||||
compatible = "semtech,lr2021";
|
||||
|
||||
@@ -37,7 +37,14 @@ int haptic_init(void)
|
||||
|
||||
haptic_dev = DEVICE_DT_GET(HAPTIC_NODE);
|
||||
if (!device_is_ready(haptic_dev)) {
|
||||
LOG_INF("no haptic driver found");
|
||||
/* The DRV2605 driver logs its own reason at debug level, so say
|
||||
* enough here to tell "chip absent" from "bus down". */
|
||||
const struct device *bus = DEVICE_DT_GET(DT_BUS(HAPTIC_NODE));
|
||||
|
||||
LOG_WRN("haptic %s not ready (addr 0x%02x, bus %s %s) — "
|
||||
"build with CONFIG_HAPTICS_LOG_LEVEL_DBG for the cause",
|
||||
haptic_dev->name, (unsigned int)DT_REG_ADDR(HAPTIC_NODE),
|
||||
bus->name, device_is_ready(bus) ? "up" : "DOWN");
|
||||
haptic_dev = NULL;
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ struct lr20xx_data {
|
||||
|
||||
/* DIO1 stuck-HIGH detection */
|
||||
int dio1_stuck_count;
|
||||
bool tcxo_disabled; /* set when the TCXO fallback has already fired */
|
||||
|
||||
/* RX data buffer */
|
||||
uint8_t rx_buf[256];
|
||||
@@ -167,6 +168,16 @@ static lr20xx_radio_lora_cr_t cr_enum_to_lr20xx(enum lora_coding_rate cr)
|
||||
}
|
||||
}
|
||||
|
||||
/* SetTcxoMode's start_time is the deadline by which the 32 MHz oscillator must
|
||||
* be detected, counted in 32 MHz clock periods (datasheet S6.11.3) — NOT in
|
||||
* 32.768 kHz RTC ticks, which is what every other timeout on this chip uses.
|
||||
* Getting that wrong turns a 5 ms allowance into 5 us, no TCXO starts that
|
||||
* fast, and the chip raises HF_XOSC_START_ERR exactly as documented. */
|
||||
static inline uint32_t tcxo_start_time_periods(uint32_t delay_ms)
|
||||
{
|
||||
return delay_ms * 32000U; /* 32000 periods per ms at 32 MHz */
|
||||
}
|
||||
|
||||
static lr20xx_system_tcxo_supply_voltage_t get_tcxo_voltage(uint16_t mv)
|
||||
{
|
||||
if (mv >= 3300) return LR20XX_SYSTEM_TCXO_CTRL_3_3V;
|
||||
@@ -354,7 +365,7 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data,
|
||||
/* Timeout in RTC ticks (30.52 µs/tick) */
|
||||
lr20xx_system_set_tcxo_mode(ctx,
|
||||
get_tcxo_voltage(cfg->tcxo_voltage_mv),
|
||||
(cfg->tcxo_startup_delay_ms * 1000U) / 31U);
|
||||
tcxo_start_time_periods(cfg->tcxo_startup_delay_ms));
|
||||
}
|
||||
|
||||
/* LDO mode — no cfg_lfclk, no set_reg_mode, no DCDC workarounds */
|
||||
@@ -399,9 +410,20 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data,
|
||||
|
||||
static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz)
|
||||
{
|
||||
lr20xx_radio_common_front_end_calibration_value_t fe_cal = {
|
||||
.rx_path = LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
.frequency_in_hertz = freq_hz,
|
||||
/* All three frequency slots are sent, the unused two as 0, so CalibFE
|
||||
* gets its full 8-byte form. The datasheet allows both "not providing"
|
||||
* the extra slots and "setting other calibration frequencies to 0", but
|
||||
* a short 4-byte command is the one thing that lines up with the
|
||||
* CMD_PERR observed across exactly this call, with a clean error word
|
||||
* and the chip in STBY_RC (so not a mode violation). Zeroed slots are
|
||||
* documented as no-ops. */
|
||||
lr20xx_radio_common_front_end_calibration_value_t fe_cal[3] = {
|
||||
{ .rx_path = LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
.frequency_in_hertz = freq_hz },
|
||||
{ .rx_path = LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
.frequency_in_hertz = 0 },
|
||||
{ .rx_path = LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
.frequency_in_hertz = 0 },
|
||||
};
|
||||
|
||||
for (int i = 0; i < LR20XX_MAX_CAL_ATTEMPTS; i++) {
|
||||
@@ -410,7 +432,7 @@ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz)
|
||||
|
||||
lr20xx_system_clear_errors(ctx);
|
||||
|
||||
rc = lr20xx_radio_common_calibrate_front_end_helper(ctx, &fe_cal, 1);
|
||||
rc = lr20xx_radio_common_calibrate_front_end_helper(ctx, fe_cal, 3);
|
||||
lr20xx_system_get_errors(ctx, &errs);
|
||||
|
||||
if (rc == LR20XX_STATUS_OK &&
|
||||
@@ -440,6 +462,29 @@ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz)
|
||||
|
||||
/* ── Apply modem configuration ──────────────────────────────────────── */
|
||||
|
||||
/* Bring-up only: read the chip's own command status and name the command that
|
||||
* failed. Return codes from the SDK reflect the SPI write, not whether the chip
|
||||
* accepted the command, so a rejection is otherwise invisible until it shows up
|
||||
* as a CmdError IRQ several commands later. */
|
||||
#if IS_ENABLED(CONFIG_LOG)
|
||||
static void lr20xx_check_cmd(void *ctx, const char *what)
|
||||
{
|
||||
lr20xx_system_stat1_t s1 = {0};
|
||||
|
||||
if (lr20xx_system_get_status(ctx, &s1, NULL, NULL) != LR20XX_STATUS_OK) {
|
||||
return;
|
||||
}
|
||||
/* 2 = CMD_OK, 3 = CMD_DAT (successful read) */
|
||||
if (s1.command_status != 2 && s1.command_status != 3) {
|
||||
LOG_ERR("command REJECTED after %s: cmd=%d", what,
|
||||
s1.command_status);
|
||||
}
|
||||
}
|
||||
#define CHECK_CMD(ctx, what) lr20xx_check_cmd((ctx), (what))
|
||||
#else
|
||||
#define CHECK_CMD(ctx, what) do { } while (0)
|
||||
#endif
|
||||
|
||||
static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
const struct lr20xx_config *cfg,
|
||||
bool tx_mode)
|
||||
@@ -448,8 +493,21 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
struct lora_modem_config *mc = &data->modem_cfg;
|
||||
lr20xx_status_t rc;
|
||||
|
||||
/* Half of what follows is mode-restricted, and this runs on the TX path
|
||||
* while the chip may still be in RX:
|
||||
* SetPacketType — "only works when the chip is in Standby RC, Standby
|
||||
* Xosc, or Fs mode", and must come first in a setup
|
||||
* CalibFE — "does not work if device is in Rx or Tx mode"
|
||||
* Both answer CMD_FAIL from the wrong mode, which surfaces only as a
|
||||
* CmdError IRQ several commands later. Force standby so the sequence is
|
||||
* always issued from a legal mode, as RadioLib does before its own
|
||||
* mode-restricted calls. */
|
||||
lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC);
|
||||
CHECK_CMD(ctx, "set_standby");
|
||||
|
||||
rc = lr20xx_radio_common_set_pkt_type(ctx, LR20XX_RADIO_COMMON_PKT_TYPE_LORA);
|
||||
LOG_DBG("modem_cfg: set_pkt_type=%d", rc);
|
||||
CHECK_CMD(ctx, "set_pkt_type");
|
||||
|
||||
/* Front-end calibration paired with set_rf_freq, exactly like RadioLib's
|
||||
* setFrequency() (cal THEN set, together). One cal covers ±50MHz so the
|
||||
@@ -461,15 +519,17 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
|
||||
rc = lr20xx_radio_common_set_rf_freq(ctx, mc->frequency);
|
||||
LOG_DBG("modem_cfg: set_rf_freq(%u)=%d", mc->frequency, rc);
|
||||
CHECK_CMD(ctx, "set_rf_freq");
|
||||
|
||||
/* Always configure the RX path after setting frequency
|
||||
* (reference does this on every set_rf_freq call). */
|
||||
rc = lr20xx_radio_common_set_rx_path(
|
||||
ctx, LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
data->rx_boost_enabled
|
||||
? LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_4
|
||||
? LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7
|
||||
: LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_NONE);
|
||||
data->rx_boost_applied = data->rx_boost_enabled;
|
||||
CHECK_CMD(ctx, "set_rx_path");
|
||||
|
||||
/* PPM offset is this chip's name for LDRO and occupies the same wire
|
||||
* field. The Semtech "recommended offset" helper only ever enables it
|
||||
@@ -488,6 +548,7 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
rc = lr20xx_radio_lora_set_modulation_params(ctx, &mod);
|
||||
LOG_DBG("modem_cfg: set_mod(SF%d BW%d CR%d PPM%d)=%d",
|
||||
mod.sf, mod.bw, mod.cr, mod.ppm, rc);
|
||||
CHECK_CMD(ctx, "set_mod_params");
|
||||
|
||||
/* DCDC workaround removed — LDO mode, RadioLib doesn't do it */
|
||||
|
||||
@@ -504,11 +565,13 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
LOG_DBG("modem_cfg: set_pkt(pre=%d len=%d crc=%d iq=%d)=%d",
|
||||
pkt.preamble_len_in_symb, pkt.pld_len_in_bytes,
|
||||
pkt.crc, pkt.iq, rc);
|
||||
CHECK_CMD(ctx, "set_pkt_params");
|
||||
|
||||
rc = lr20xx_radio_lora_set_syncword(ctx,
|
||||
mc->public_network ? 0x34 : 0x12);
|
||||
LOG_DBG("modem_cfg: set_syncword(0x%02x)=%d",
|
||||
mc->public_network ? 0x34 : 0x12, rc);
|
||||
CHECK_CMD(ctx, "set_syncword");
|
||||
|
||||
if (tx_mode) {
|
||||
/* PA config + TX params from RadioLib's known-good LF table.
|
||||
@@ -528,10 +591,24 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
pa_val, rc);
|
||||
}
|
||||
|
||||
/* Only the events the DIO1 handler actually acts on. PREAMBLE_DETECTED
|
||||
* and SYNC_WORD_HEADER_VALID are deliberately NOT here: they fire on
|
||||
* noise, and an unhandled DIO1 assertion drives the safety path, which
|
||||
* restarts RX and destroys the very packet that was arriving. They stay
|
||||
* readable in the IRQ register for lr20xx_is_receiving(), which is the
|
||||
* same split the SX126x driver uses for its RX-busy gate. */
|
||||
rc = lr20xx_system_set_dio_irq_cfg(ctx, lr20xx_irq_dio(cfg),
|
||||
LR20XX_SYSTEM_IRQ_ALL_MASK &
|
||||
~(LR20XX_SYSTEM_IRQ_FIFO_RX | LR20XX_SYSTEM_IRQ_FIFO_TX));
|
||||
LR20XX_SYSTEM_IRQ_RX_DONE |
|
||||
LR20XX_SYSTEM_IRQ_TX_DONE |
|
||||
LR20XX_SYSTEM_IRQ_CAD_DONE |
|
||||
LR20XX_SYSTEM_IRQ_CAD_DETECTED |
|
||||
LR20XX_SYSTEM_IRQ_TIMEOUT |
|
||||
LR20XX_SYSTEM_IRQ_CRC_ERROR |
|
||||
LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR |
|
||||
LR20XX_SYSTEM_IRQ_ERROR |
|
||||
LR20XX_SYSTEM_IRQ_CMD_ERROR);
|
||||
LOG_DBG("modem_cfg: set_dio_irq=%d", rc);
|
||||
CHECK_CMD(ctx, "set_dio_irq");
|
||||
|
||||
DUMP_CHIP_STATE(ctx, &data->hal_ctx, tx_mode ? "modem-TX" : "modem-RX");
|
||||
}
|
||||
@@ -550,6 +627,7 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data)
|
||||
if (data->dc_rx_ms == 0 || data->dc_sleep_ms == 0) {
|
||||
LOG_WRN("No duty-cycle timing stored — continuous RX");
|
||||
data->rx_duty_cycle_enabled = false;
|
||||
data->hal_ctx.auto_sleeps = false;
|
||||
lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(
|
||||
ctx, 0xFFFFFF);
|
||||
return false;
|
||||
@@ -617,6 +695,46 @@ static void lr20xx_restart_rx(struct lr20xx_data *data)
|
||||
|
||||
lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
|
||||
|
||||
/* TX rewrites payload_len to the length it sent, and in explicit-header
|
||||
* RX that field is a filter: "accept 1..payload_len, reject anything
|
||||
* longer with a header error". Left alone, a node silently stops hearing
|
||||
* every packet bigger than its own last transmission. RadioLib restores
|
||||
* this on each RX entry for the same reason. */
|
||||
{
|
||||
lr20xx_radio_lora_pkt_params_t pkt = {
|
||||
.preamble_len_in_symb = data->modem_cfg.preamble_len,
|
||||
.pkt_mode = LR20XX_RADIO_LORA_PKT_EXPLICIT,
|
||||
.pld_len_in_bytes = 255,
|
||||
.crc = data->modem_cfg.packet_crc_disable
|
||||
? LR20XX_RADIO_LORA_CRC_DISABLED
|
||||
: LR20XX_RADIO_LORA_CRC_ENABLED,
|
||||
.iq = data->modem_cfg.iq_inverted
|
||||
? LR20XX_RADIO_LORA_IQ_INVERTED
|
||||
: LR20XX_RADIO_LORA_IQ_STANDARD,
|
||||
};
|
||||
lr20xx_radio_lora_set_packet_params(ctx, &pkt);
|
||||
CHECK_CMD(ctx, "restart_rx set_pkt_params");
|
||||
}
|
||||
|
||||
/* "If the device is already in Rx mode, the command fails as timeout
|
||||
* cannot be updated" (datasheet, SetRx) — and in continuous mode the
|
||||
* chip never leaves Rx, it just keeps searching. So after a packet there
|
||||
* is nothing to re-arm: issuing SetRx only earns a CMD_FAIL, whose
|
||||
* CmdError comes back through DIO1 as another unhandled IRQ.
|
||||
*
|
||||
* Skipping it also keeps the receiver on air. Forcing a mode change
|
||||
* would cost the HF oscillator restart (702 us) plus PLL lock (32 us)
|
||||
* on the path taken after every single packet. */
|
||||
if (!data->rx_duty_cycle_enabled) {
|
||||
lr20xx_system_stat2_t s2 = {0};
|
||||
|
||||
if (lr20xx_system_get_status(ctx, NULL, &s2, NULL) == LR20XX_STATUS_OK &&
|
||||
s2.chip_mode == LR20XX_SYSTEM_CHIP_MODE_RX) {
|
||||
data->in_rx_mode = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (data->rx_duty_cycle_enabled) {
|
||||
lr20xx_apply_rx_duty_cycle(data);
|
||||
} else {
|
||||
@@ -624,6 +742,7 @@ static void lr20xx_restart_rx(struct lr20xx_data *data)
|
||||
ctx, 0xFFFFFF);
|
||||
}
|
||||
|
||||
CHECK_CMD(ctx, "restart_rx set_rx");
|
||||
data->in_rx_mode = true;
|
||||
}
|
||||
|
||||
@@ -840,12 +959,20 @@ static void lr20xx_dio1_work_handler(struct k_work *work)
|
||||
}
|
||||
|
||||
safety_check:
|
||||
/* A zero IRQ word means the edge was already consumed — a duplicate
|
||||
* re-submit, or the restart path cleared it. Nothing failed, so do not
|
||||
* tear RX down and do not count it toward the stuck-DIO1 reset. */
|
||||
if (irq == 0) {
|
||||
goto edge_recheck;
|
||||
}
|
||||
|
||||
if (!rx_restarted && data->in_rx_mode && !data->tx_active) {
|
||||
LOG_WRN("DIO1 safety: no IRQ handled (0x%08x rc=%d), "
|
||||
"restarting RX", irq, rc);
|
||||
lr20xx_restart_rx(data);
|
||||
}
|
||||
|
||||
edge_recheck:
|
||||
/* Edge-triggered DIO1: if still HIGH, re-submit for pending flags.
|
||||
* Guard against stuck DIO1: after 5 empty cycles, hardware reset. */
|
||||
if (gpio_pin_get_dt(&data->hal_ctx.dio1)) {
|
||||
@@ -1182,7 +1309,7 @@ void lr20xx_set_rx_boost(const struct device *dev, bool enable)
|
||||
k_mutex_lock(&data->spi_mutex, K_FOREVER);
|
||||
lr20xx_radio_common_set_rx_path(
|
||||
&data->hal_ctx, LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
enable ? LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_4
|
||||
enable ? LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7
|
||||
: LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_NONE);
|
||||
data->rx_boost_applied = enable;
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
@@ -1234,7 +1361,7 @@ void lr20xx_reset_agc(const struct device *dev)
|
||||
if (data->rx_boost_enabled) {
|
||||
lr20xx_radio_common_set_rx_path(
|
||||
ctx, LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_4);
|
||||
LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7);
|
||||
data->rx_boost_applied = true;
|
||||
}
|
||||
|
||||
@@ -1243,20 +1370,25 @@ void lr20xx_reset_agc(const struct device *dev)
|
||||
|
||||
/* ── Driver API: CAD ────────────────────────────────────────────────── */
|
||||
|
||||
/* Recommended cad_detect_peak values per SF for 2-symbol CAD.
|
||||
* From Semtech LR20xx datasheet table. Using 2 symbols as a
|
||||
* good balance between speed (~2 symbol durations) and reliability. */
|
||||
/* Datasheet Table 6-19, the 4-symbol row — LoRaRadioBase asks for
|
||||
* LORA_CAD_SYMB_4, so these are the values that go with the window we
|
||||
* actually use. The old table here was RadioLib's 2-symbol set, which is
|
||||
* systematically higher: a higher det_peak is a *less* sensitive CAD, so
|
||||
* pairing it with a 4-symbol window made LBT more willing to talk over
|
||||
* faint traffic. Adaptive CAD offsets from this base, so getting the base
|
||||
* right shifts the whole operating range. */
|
||||
static uint8_t lr20xx_cad_detect_peak(uint8_t sf)
|
||||
{
|
||||
switch (sf) {
|
||||
case 5: case 6: return 56;
|
||||
case 7: return 56;
|
||||
case 8: return 58;
|
||||
case 9: return 58;
|
||||
case 10: return 60;
|
||||
case 11: return 64;
|
||||
case 12: return 68;
|
||||
default: return 60;
|
||||
case 5: return 51;
|
||||
case 6: return 51;
|
||||
case 7: return 51;
|
||||
case 8: return 54;
|
||||
case 9: return 56;
|
||||
case 10: return 60;
|
||||
case 11: return 60;
|
||||
case 12: return 64;
|
||||
default: return 56;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,7 +1399,7 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
|
||||
|
||||
uint8_t sf = (uint8_t)mc->datarate;
|
||||
lr20xx_radio_lora_cad_params_t cad = {
|
||||
.cad_symb_nb = 2,
|
||||
.cad_symb_nb = 4, /* overridden below from mc->cad.symbol_num */
|
||||
.pnr_delta = 0, /* exact symbol count, no best-effort */
|
||||
.cad_exit_mode = LR20XX_RADIO_LORA_CAD_EXIT_MODE_STANDBYRC,
|
||||
.cad_timeout_in_pll_step = 0,
|
||||
@@ -1298,11 +1430,13 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
|
||||
}
|
||||
|
||||
lr20xx_radio_lora_configure_cad_params(ctx, &cad);
|
||||
CHECK_CMD(ctx, "configure_cad_params");
|
||||
|
||||
/* Clear any pending IRQ flags, then start CAD */
|
||||
lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
|
||||
data->cad_active = true;
|
||||
lr20xx_radio_lora_set_cad(ctx);
|
||||
CHECK_CMD(ctx, "set_cad");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1475,6 +1609,7 @@ static int lr20xx_lora_recv_duty_cycle(const struct device *dev,
|
||||
data->dc_rx_ms = rx_ms;
|
||||
data->dc_sleep_ms = slp_ms;
|
||||
data->rx_duty_cycle_enabled = true;
|
||||
data->hal_ctx.auto_sleeps = true;
|
||||
lr20xx_radio_common_set_rx_duty_cycle(ctx, rx_ms, slp_ms,
|
||||
LR20XX_RADIO_COMMON_RX_DUTY_CYCLE_MODE_RX);
|
||||
LOG_INF("recv_duty_cycle: rx=%ums sleep=%ums", rx_ms, slp_ms);
|
||||
@@ -1524,6 +1659,33 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
|
||||
|
||||
LOG_INF("LR20xx SDK get_version: major=%u minor=%u", ver.major, ver.minor);
|
||||
|
||||
#if IS_ENABLED(CONFIG_LOG)
|
||||
/* GET_VERSION (0x0101) must answer 0x01 0x18 for the documented FW 1.24.
|
||||
* Two dumps, because the two references disagree on the protocol and
|
||||
* our layout matches RadioLib on paper yet reads shifted on silicon:
|
||||
*
|
||||
* frame A = command and response in ONE NSS window (RadioLib's model)
|
||||
* frame B = a bare read in a SECOND window (the Semtech HAL's model)
|
||||
*
|
||||
* Whichever one contains 01 18 is the protocol this chip actually
|
||||
* speaks, and its offset is the skip the HAL should use.
|
||||
*/
|
||||
{
|
||||
const uint8_t ver_cmd[2] = { 0x01, 0x01 };
|
||||
uint8_t follow[8] = { 0 };
|
||||
|
||||
lr20xx_hal_debug_raw_frame(ctx, ver_cmd, sizeof(ver_cmd), 10);
|
||||
|
||||
if (lr20xx_hal_direct_read(ctx, follow, sizeof(follow)) ==
|
||||
LR20XX_HAL_STATUS_OK) {
|
||||
LOG_HEXDUMP_INF(follow, sizeof(follow),
|
||||
"frame B: bare read after GET_VERSION");
|
||||
} else {
|
||||
LOG_WRN("frame B: bare read failed");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The only base FW version the datasheet documents is 1.24 (0x01/0x18).
|
||||
* Anything else still runs — the mismatch is worth a line in the log
|
||||
* when a bring-up goes sideways, not a hard failure. */
|
||||
@@ -1538,8 +1700,15 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
|
||||
* needed when SetRegMode simo_usage=0x02 (SIMO_NORMAL).
|
||||
* We run in LDO mode (default, simo_usage=0x00). */
|
||||
|
||||
if (cfg->tcxo_voltage_mv > 0) {
|
||||
uint32_t tcxo_ticks = (cfg->tcxo_startup_delay_ms * 1000U) / 31U;
|
||||
/* A board that declares a TCXO but is actually fitted with a plain
|
||||
* crystal fails to start its 32 MHz reference once TCXO mode is on, and
|
||||
* every later command is rejected. The datasheet (S1.9.3) puts the TCXO
|
||||
* on XTA with the VTCXO regulator, so the two builds are indistinguishable
|
||||
* from software — the chip's own HF_XOSC_START error is the only signal.
|
||||
* Fall back to XTAL rather than come up dead, as MeshCore's CustomLR2021
|
||||
* does when begin() reports a rejected command. */
|
||||
if (cfg->tcxo_voltage_mv > 0 && !data->tcxo_disabled) {
|
||||
uint32_t tcxo_ticks = tcxo_start_time_periods(cfg->tcxo_startup_delay_ms);
|
||||
lr20xx_status_t tcxo_rc = lr20xx_system_set_tcxo_mode(ctx,
|
||||
get_tcxo_voltage(cfg->tcxo_voltage_mv),
|
||||
tcxo_ticks);
|
||||
@@ -1590,6 +1759,23 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
|
||||
|
||||
DUMP_CHIP_STATE(ctx, &data->hal_ctx, "post-cal");
|
||||
|
||||
/* The 32 MHz reference is what calibration needs, so this is where a
|
||||
* wrong clock source shows up. One retry only — if XTAL fails too, the
|
||||
* fault is not the clock config and looping would just hide it. */
|
||||
{
|
||||
lr20xx_system_errors_t clk_err = 0;
|
||||
|
||||
lr20xx_system_get_errors(ctx, &clk_err);
|
||||
if ((clk_err & LR20XX_SYSTEM_ERRORS_HF_XOSC_START_MASK) &&
|
||||
cfg->tcxo_voltage_mv > 0 && !data->tcxo_disabled) {
|
||||
LOG_WRN("HF XOSC did not start with TCXO at %d mV "
|
||||
"(errors=0x%04x) — retrying in XTAL mode",
|
||||
cfg->tcxo_voltage_mv, clk_err);
|
||||
data->tcxo_disabled = true;
|
||||
return lr20xx_hw_init(data, cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Front-end calibration at 868 MHz LF.
|
||||
* raw_value = ceil(868000000/4000000) = 217 = 0x00D9 */
|
||||
st = lr20xx_calibrate_front_end(ctx, 868000000);
|
||||
@@ -1606,6 +1792,32 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
|
||||
|
||||
DUMP_CHIP_STATE(ctx, &data->hal_ctx, "init-done");
|
||||
|
||||
#if IS_ENABLED(CONFIG_LOG)
|
||||
/* Does merely polling status raise CmdError? get_status is a bare
|
||||
* direct_read (no opcode) in this SDK, where RadioLib sends GET_STATUS
|
||||
* as a real command — so if the chip parses those clocked-out zeros as
|
||||
* a malformed command, our own polling is manufacturing the CMD_ERROR
|
||||
* storm rather than reporting one. Chip is idle here, so nothing else
|
||||
* can set the bit. */
|
||||
{
|
||||
lr20xx_system_irq_mask_t before = 0, after = 0;
|
||||
lr20xx_system_stat1_t s1 = {0};
|
||||
|
||||
lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
|
||||
lr20xx_system_get_status(ctx, NULL, NULL, &before);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
lr20xx_system_get_status(ctx, &s1, NULL, NULL);
|
||||
}
|
||||
|
||||
lr20xx_system_get_status(ctx, NULL, NULL, &after);
|
||||
LOG_INF("status-poll probe: irq before=0x%08x after=0x%08x "
|
||||
"(CMD_ERROR %s self-inflicted)", before, after,
|
||||
(after & LR20XX_SYSTEM_IRQ_CMD_ERROR) ? "IS" : "is NOT");
|
||||
lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* DEBUG: what voltage does the chip see on its OWN supply pin? (mV,
|
||||
* after MU calibration). If this reads low (≪3000mV) while the board
|
||||
* is powered, the LR2021 VBAT/supply pin is floating or miswired — that
|
||||
|
||||
@@ -30,13 +30,20 @@ properties:
|
||||
type: int
|
||||
default: 0
|
||||
description: |
|
||||
TCXO supply voltage in millivolts (provided by LR2021 DIO3).
|
||||
0 = no TCXO (use XTAL). Common: 1800 (1.8V), 3300 (3.3V).
|
||||
TCXO supply voltage in millivolts. On LR2021 the TCXO sits on XTA and is
|
||||
fed by the internal regulator via the VTCXO pin (datasheet S1.9.3) — not
|
||||
DIO3, despite what the SX126x-derived names elsewhere suggest.
|
||||
0 = no TCXO (use XTAL). Allowed: 1600, 1700, 1800, 2200, 2400, 2700,
|
||||
3000, 3300.
|
||||
|
||||
tcxo-startup-delay-ms:
|
||||
type: int
|
||||
default: 5
|
||||
description: TCXO startup stabilization time in milliseconds.
|
||||
default: 10
|
||||
description: |
|
||||
Deadline for the 32 MHz TCXO to start and stabilise, in milliseconds.
|
||||
This is a timeout, not a fixed wait, so erring high only delays how
|
||||
quickly a dead oscillator is reported. Too low and the chip raises
|
||||
HF_XOSC_START_ERR and rejects every later command.
|
||||
|
||||
rx-boosted:
|
||||
type: boolean
|
||||
|
||||
Reference in New Issue
Block a user