From 9f66e9749c33abf108132247a0dc52fd1eff6ecb Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:55:13 +0200 Subject: [PATCH] x1 debug v1 --- .gitignore | 3 + gen_provider_catalog.py | 6 + zephcore/adapters/gps/ZephyrGPSManager.cpp | 32 +-- .../adapters/radio/lr20xx/lr20xx_hal_zephyr.c | 151 ++++++++-- .../adapters/radio/lr20xx/lr20xx_hal_zephyr.h | 10 + .../radio/lr20xx/lr20xx_radio_common.c | 7 +- .../boards/nrf52840/meshtracker_x1/board.conf | 6 + .../nrf52840/meshtracker_x1/bringup.conf | 29 ++ .../meshtracker_x1_nrf52840.dts | 15 +- .../promicro_lr2021/promicro_lr2021.dts | 2 + zephcore/helpers/ui/haptic.c | 9 +- .../drivers/lora/lr20xx/lr20xx_lora.c | 260 ++++++++++++++++-- .../dts/bindings/lora/semtech,lr2021.yaml | 15 +- 13 files changed, 467 insertions(+), 78 deletions(-) create mode 100644 zephcore/boards/nrf52840/meshtracker_x1/bringup.conf diff --git a/.gitignore b/.gitignore index 88401f3..c20d2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/gen_provider_catalog.py b/gen_provider_catalog.py index 814b609..8e785d4 100644 --- a/gen_provider_catalog.py +++ b/gen_provider_catalog.py @@ -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"), diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 33dd3d9..e47f119 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -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 */ diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c b/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c index a390ad0..7034256 100644 --- a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c +++ b/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c @@ -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; diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h b/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h index 4025130..f0ce8bd 100644 --- a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h +++ b/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h @@ -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 diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c b/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c index 54af91b..a8b979d 100644 --- a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c +++ b/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c @@ -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 ); diff --git a/zephcore/boards/nrf52840/meshtracker_x1/board.conf b/zephcore/boards/nrf52840/meshtracker_x1/board.conf index c5089d0..f1ef4ba 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/board.conf +++ b/zephcore/boards/nrf52840/meshtracker_x1/board.conf @@ -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 diff --git a/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf b/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf new file mode 100644 index 0000000..1f2b8ca --- /dev/null +++ b/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf @@ -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 diff --git a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts index ca7ac05..3c175e6 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts +++ b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts @@ -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 = ; 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"; diff --git a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts index 526eeaf..d958c65 100644 --- a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts +++ b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts @@ -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"; diff --git a/zephcore/helpers/ui/haptic.c b/zephcore/helpers/ui/haptic.c index 627b098..51cfb21 100644 --- a/zephcore/helpers/ui/haptic.c +++ b/zephcore/helpers/ui/haptic.c @@ -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; } diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c index 1a570fa..660233e 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -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 diff --git a/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml b/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml index a3b8105..aa87962 100644 --- a/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml +++ b/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml @@ -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