From 557aef1883ed52212c0ed6f6ee6f3f793bda4a50 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:43:53 +0200 Subject: [PATCH] qspi probe --- zephcore/CMakeLists.txt | 2 + zephcore/Kconfig | 10 ++ .../boards/nrf52840/meshtracker_x1/board.conf | 9 +- .../nrf52840/meshtracker_x1/bringup.conf | 26 +++- .../meshtracker_x1_nrf52840.dts | 23 +++- zephcore/helpers/qspi_probe.c | 123 ++++++++++++++++++ 6 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 zephcore/helpers/qspi_probe.c diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index f4d36da..14615d8 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -574,6 +574,8 @@ target_sources(app PRIVATE # Notification-mode gate. Same reason as led_gate: the CLI needs these # symbols on boards that compile no buzzer at all. helpers/buzzer_gate.c + # QSPI bring-up probe. Self-stubs unless CONFIG_ZEPHCORE_QSPI_RDID_PROBE. + helpers/qspi_probe.c ) # Boot-time hardware-RTC auto-discovery (compact raw-I2C). Always compiled so diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 8948378..ff1a92d 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -51,6 +51,16 @@ source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" menu "ZephCore" +config ZEPHCORE_QSPI_RDID_PROBE + bool "QSPI flash bring-up probe" + help + Five seconds after boot, report whether the QSPI flash driver came + up. If it did not, print the init errno the driver discards and + re-read the JEDEC id by bit-banging the pins as single-lane SPI, + which distinguishes a driver-side fault from a part that is not + answering. Skipped entirely when the flash is working, so it is safe + to leave enabled. Bring-up diagnostic. + config ZEPHCORE_RESET_ON_FATAL_ERROR bool "Reboot on fatal error" default y diff --git a/zephcore/boards/nrf52840/meshtracker_x1/board.conf b/zephcore/boards/nrf52840/meshtracker_x1/board.conf index f1ef4ba..0b0a8b8 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/board.conf +++ b/zephcore/boards/nrf52840/meshtracker_x1/board.conf @@ -22,8 +22,7 @@ CONFIG_ZEPHCORE_RADIO_LR2021=y 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 +# The flash sits behind the P0.15 power switch, modelled as a regulator so its +# power-up time is a startup-delay rather than a race. Regulators come up at 75; +# probe the flash after that. +CONFIG_NORDIC_QSPI_NOR_INIT_PRIORITY=80 diff --git a/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf b/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf index 1f2b8ca..06fad24 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf +++ b/zephcore/boards/nrf52840/meshtracker_x1/bringup.conf @@ -23,7 +23,25 @@ CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL_DBG=y 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 +# Rescue the pre-enumeration boot log. +# +# Drivers that fail at POST_KERNEL report it around 50 ms, but USB CDC does not +# enumerate until ~800 ms, so the log thread drains those messages into a +# console nobody is attached to and they are gone. That is why the QSPI NOR +# driver's JEDEC error has never once been seen, despite it being the one line +# that says what the flash actually answered. +# +# Logging is already deferred, so holding the processing thread back simply +# leaves early messages in the buffer until the console exists to receive them. +CONFIG_LOG_PROCESS_THREAD_STARTUP_DELAY_MS=2500 + +# Overflow drops the *oldest* message, which here is exactly the one worth +# keeping. Inverted: keep the earliest, drop later ones if it fills. Only the +# first 2.5 s is buffered — after the flush the thread drains continuously. +CONFIG_LOG_MODE_OVERFLOW=n + +# Must hold that whole 2.5 s window now rather than a moment of it. +CONFIG_LOG_BUFFER_SIZE=8192 + +# Did the flash driver come up, and if not, why. Skips itself when /ext works. +CONFIG_ZEPHCORE_QSPI_RDID_PROBE=y diff --git a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts index 3c175e6..b1840f3 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts +++ b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts @@ -100,6 +100,22 @@ startup-delay-us = <5000>; }; + /* P0.15 — 3V3 rail feeding the QSPI flash. + * + * A regulator rather than a gpio-hog so the part's power-up time is an + * enforced delay instead of a race: a hog raises the rail and returns, + * and the flash driver a few init priorities later clocks out RDID + * microseconds afterwards — before the switch and the rail's bulk + * capacitance have settled. The read comes back as 0x00/0xff, the JEDEC + * compare fails, and /ext never mounts. */ + flash_power: flash-power { + compatible = "regulator-fixed"; + regulator-name = "flash-power"; + enable-gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; + regulator-boot-on; + startup-delay-us = <10000>; + }; + /* P1.06 — gates the battery ADC divider, enabled on demand */ vbat_enable: vbat-enable { compatible = "regulator-fixed"; @@ -224,13 +240,6 @@ gpios = <9 GPIO_ACTIVE_HIGH>; output-high; }; - - /* QSPI flash power switch (P0.15) */ - flash-power-on { - gpio-hog; - gpios = <15 GPIO_ACTIVE_HIGH>; - output-high; - }; }; &gpio1 { diff --git a/zephcore/helpers/qspi_probe.c b/zephcore/helpers/qspi_probe.c new file mode 100644 index 0000000..1f25168 --- /dev/null +++ b/zephcore/helpers/qspi_probe.c @@ -0,0 +1,123 @@ +/* + * ZephCore - QSPI flash bring-up probe + * Copyright (c) 2026 ZephCore + * SPDX-License-Identifier: MIT + * + * Zephyr's nrf_qspi_nor driver throws away the reason it failed: qspi_init() + * does `rc = nrfx_qspi_init() / exit_dpd() / qspi_rdid(); if (rc < 0) return + * rc;` and only a JEDEC *mismatch* is logged. A flash that never answers looks + * exactly like one that was never probed, and /ext just fails with -ENODEV. + * + * Two things are recovered here. The init return code is not actually lost — + * a failed init is recorded in the device state as a positive errno, which + * says which of those three steps broke. And RDID (0x9F) is re-sent by + * bit-banging the QSPI pins as ordinary single-lane SPI, which needs nothing + * from the QSPI peripheral and so still answers when the driver has given up: + * c8 40 17 - the expected GD25Q64E, so the fault is driver-side + * 00 00 00 - pins reach the part but it is not driving (unpowered? early?) + * ff ff ff - nothing on the bus at all (wrong pins / no part) + * + * Enable with CONFIG_ZEPHCORE_QSPI_RDID_PROBE. + */ + +#include +#include +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZEPHCORE_QSPI_RDID_PROBE) && \ + DT_HAS_COMPAT_STATUS_OKAY(nordic_qspi_nor) + +LOG_MODULE_REGISTER(qspi_probe, CONFIG_FLASH_LOG_LEVEL); + +/* Pin numbers, not gpio-specs: these live in the board's qspi pinctrl group, + * which cannot be referenced as a phandle. Defaults match the MeshTracker X1. */ +#define P_SCK 19 +#define P_CSN 20 +#define P_IO0 21 +#define P_IO1 22 + +static const struct device *p0; + +static uint8_t spi_byte(uint8_t out) +{ + uint8_t in = 0; + + for (int b = 7; b >= 0; b--) { + gpio_pin_set_raw(p0, P_IO0, (out >> b) & 1); + k_busy_wait(1); + gpio_pin_set_raw(p0, P_SCK, 1); /* mode 0: sample on rising */ + in = (in << 1) | (gpio_pin_get_raw(p0, P_IO1) & 1); + k_busy_wait(1); + gpio_pin_set_raw(p0, P_SCK, 0); + } + return in; +} + +static int qspi_rdid_probe(void) +{ + const struct device *flash = + DEVICE_DT_GET(DT_COMPAT_GET_ANY_STATUS_OKAY(nordic_qspi_nor)); + uint8_t id[3]; + + p0 = DEVICE_DT_GET(DT_NODELABEL(gpio0)); + if (!device_is_ready(p0)) { + return 0; + } + + if (device_is_ready(flash)) { + /* Bit-banging steals the QSPI pins as GPIOs and leaves them + * that way, which would break a flash that just came up fine. + * There is nothing to diagnose in that case, so stay off the + * bus and let this probe live in a build that works. */ + LOG_INF("qspi flash '%s' ready - /ext usable, RDID probe skipped", + flash->name); + return 0; + } + + LOG_INF("qspi flash '%s' failed init (init_res=-%d) - bit-banging RDID on " + "SCK=P0.%02d CSN=P0.%02d IO0=P0.%02d IO1=P0.%02d", + flash->name, (int)flash->state->init_res, + P_SCK, P_CSN, P_IO0, P_IO1); + + gpio_pin_configure(p0, P_CSN, GPIO_OUTPUT_HIGH); + gpio_pin_configure(p0, P_SCK, GPIO_OUTPUT_LOW); + gpio_pin_configure(p0, P_IO0, GPIO_OUTPUT_LOW); + gpio_pin_configure(p0, P_IO1, GPIO_INPUT); + k_busy_wait(100); + + gpio_pin_set_raw(p0, P_CSN, 0); + (void)spi_byte(0x9F); + id[0] = spi_byte(0x00); + id[1] = spi_byte(0x00); + id[2] = spi_byte(0x00); + gpio_pin_set_raw(p0, P_CSN, 1); + + LOG_INF("RDID raw: %02x %02x %02x (expect c8 40 17 for GD25Q64E)", + id[0], id[1], id[2]); + return 0; +} + +/* Deferred rather than run straight from SYS_INIT: at APPLICATION level this + * fires before USB CDC enumerates, so the result went to a console nobody was + * attached to yet — the same truncation that hides the driver's own JEDEC + * error. Five seconds is comfortably past enumeration and long after the flash + * driver and its supply regulator have had their turn. */ +static void qspi_probe_work(struct k_work *work) +{ + ARG_UNUSED(work); + qspi_rdid_probe(); +} + +static K_WORK_DELAYABLE_DEFINE(qspi_probe_dwork, qspi_probe_work); + +static int qspi_probe_schedule(void) +{ + k_work_schedule(&qspi_probe_dwork, K_SECONDS(5)); + return 0; +} + +SYS_INIT(qspi_probe_schedule, APPLICATION, 99); + +#endif