From 47f24a0b1c36d3124b85a11c18b65d67b05a02a7 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:26:09 +0200 Subject: [PATCH] M9 SD card settle --- releasenotes/RELEASE_NOTES_1.16.6-zephcore.md | 8 +- zephcore/CMakeLists.txt | 13 ++ .../boards/esp32/thinknode_m9/sd_quiesce.c | 137 ++++++++++++++++++ .../boards/nrf52840/muziworks_r1neo/README.md | 23 ++- .../muziworks_r1neo_nrf52840-pinctrl.dtsi | 9 +- .../muziworks_r1neo_nrf52840.dts | 16 +- 6 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 zephcore/boards/esp32/thinknode_m9/sd_quiesce.c diff --git a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md index a07ec31..39c9f5e 100644 --- a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md @@ -117,8 +117,12 @@ the screen is mirrored or upside down, that's a known one-line fix), GPS output battery calibration, and whether the USB-C port reaches the native USB pads at all (BLE is the companion path meanwhile). Not ported: the I2C keyboard, which is driven by a second MCU. -Also worth knowing on this board specifically: the **SD slot shares the radio's SPI bus**. That has no -effect on normal operation, but it does matter if you use the LR1110 updater below. +Also worth knowing on this board specifically: the **SD slot shares the radio's SPI bus** with the LR1110 +and the display. An inserted card was confirmed to break LR1110 radio-firmware flashing on this board — the +write reports success and the radio then runs nothing. To keep a card from disturbing normal operation, the +firmware now puts any inserted card into SPI idle at startup, before the radio driver initialises, so it +stays quiet until something actually addresses it. **If the M9 misbehaves in a way you can't explain, try +it once with the card removed** — that is still the fastest way to rule the slot out. Feedback from anyone with an M9 is genuinely useful here — the bring-up list in the board's README says exactly what to look at. diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 4469684..6d8f647 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -530,6 +530,19 @@ else() message(STATUS "ZephCore Battery: generic LiPo curve") endif() +# ========== Board-specific startup quirks ========== +# Optional per-board sd_quiesce.c — boards whose SD slot shares the radio's SPI +# bus put any inserted card into SPI idle before the radio driver initialises, +# so it cannot disturb the bus. Registers itself with SYS_INIT; nothing calls +# into it, so it is simply compiled in when the board provides one. +file(GLOB_RECURSE _BOARD_SD_QUIESCE + "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/sd_quiesce.c") +if(_BOARD_SD_QUIESCE) + list(GET _BOARD_SD_QUIESCE 0 _BOARD_SD_QUIESCE) + message(STATUS "ZephCore SD: shared-bus quiesce (${_BOARD_SD_QUIESCE})") + target_sources(app PRIVATE "${_BOARD_SD_QUIESCE}") +endif() + # Native-Linux (native_sim) runtime setup: force real-time clock mode so the # simulated clock tracks wall time (required for the real SX126x radio's # BUSY/DIO1 timing). Bakes in the equivalent of the --rt command-line flag. diff --git a/zephcore/boards/esp32/thinknode_m9/sd_quiesce.c b/zephcore/boards/esp32/thinknode_m9/sd_quiesce.c new file mode 100644 index 0000000..b3e8046 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/sd_quiesce.c @@ -0,0 +1,137 @@ +/* + * ThinkNode M9 — quiesce an inserted SD card before the radio comes up. + * SPDX-License-Identifier: MIT + * + * The SD slot shares SPI2 with the LR1110 and the ST7789. Its CS is parked + * HIGH by the `sd-cs-park` gpio-hog in the base DTS, which is enough for a + * card that is powered and has been initialised: it tri-states its data-out + * while deselected and ignores the bus. + * + * A card that has never been addressed is not in the second of those states: it + * comes up in native SD mode, not SPI mode, so it has not yet been told to + * ignore traffic it was not addressed for. Until something runs the SPI-mode + * entry sequence, its behaviour on a busy shared bus is simply not defined by + * the spec. + * + * The card is powered normally whenever the firmware runs, so this is NOT the + * parasitic-supply trap that VEXT once was for the LR1110. Confirmed on the + * V1.0 schematic: SD_3V3 comes off VDD_PERIPH through series R86, with no + * enable of its own, and VDD_PERIPH is the MOSFET-gated rail that GPIO18 + * (VDD_PERIPH_EN) switches — asserted at boot by the tft_pwr_enable regulator. + * + * Worth knowing when debugging this board: VDD_PERIPH fans out through one + * series resistor per consumer — R82 to VDD_RADIO, R86 to SD_3V3, R88 to + * DISPLAY_VDD, R85 to RTC_3V3. The radio and the SD slot therefore share a + * single gated rail. If an inserted card ever turns out to disturb the radio + * in a way this quiesce does not fix, supply sag under combined load is the + * next suspect, and no amount of bus discipline will address it. + * + * An inserted card was field-confirmed to break LR1110 flash programming on + * this board: every write reported OK, the chip programmed for real, and the + * image still failed its integrity check at boot with no error reported + * anywhere. This runs the standard SPI-mode entry sequence once at startup so + * any card present lands in SPI idle — a defined, quiet state where it ignores + * everything until selected — rather than whatever it powered up into. + * + * Runs at POST_KERNEL priority 70: after the SPI bus (KERNEL_INIT_PRIORITY_DEVICE + * = 50) and GPIO, before the LoRa driver (CONFIG_LORA_INIT_PRIORITY = 90), so + * the card is quiet before any radio traffic exists. + * + * Best-effort by design. Every failure path leaves CS parked HIGH exactly as + * the hog had it and returns success — a card we could not talk to is no worse + * off than one we never tried, and refusing to boot over it would be far worse + * than the problem. + */ + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(m9_sd_quiesce, CONFIG_ZEPHCORE_LOG_LEVEL); + +/* SD CS is GPIO48 = gpio1 pin 16. No DT node describes the slot — the base + * board DTS only parks this pin with a gpio-hog. */ +#define SD_CS_PORT DT_NODELABEL(gpio1) +#define SD_CS_PIN 16 + +/* The radio's bus. Taken from the lora node so this cannot drift apart from + * the devicetree if SPI2 is ever renumbered. */ +#define SD_SPI_BUS DT_BUS(DT_NODELABEL(lora)) + +static int m9_sd_quiesce(void) +{ + const struct device *spi = DEVICE_DT_GET(SD_SPI_BUS); + const struct device *cs_port = DEVICE_DT_GET(SD_CS_PORT); + /* SD initialisation is specified at 100-400 kHz; the radio keeps its + * own clock, this config is local to these few transfers. */ + struct spi_config cfg = { + .frequency = 400000, + .operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB, + }; + /* CMD0 GO_IDLE_STATE, arg 0, CRC7 0x95 — CRC is still checked in the + * card's power-up state, so it must be correct. */ + static const uint8_t cmd0[6] = { 0x40, 0x00, 0x00, 0x00, 0x00, 0x95 }; + uint8_t tx[8], rx[8]; + + if (!device_is_ready(spi) || !device_is_ready(cs_port)) { + return 0; + } + if (gpio_pin_configure(cs_port, SD_CS_PIN, GPIO_OUTPUT_HIGH) < 0) { + return 0; + } + + /* >=74 clocks with CS HIGH — the SD power-up requirement. */ + memset(tx, 0xFF, sizeof(tx)); + const struct spi_buf wake_buf = { .buf = tx, .len = sizeof(tx) }; + const struct spi_buf_set wake = { .buffers = &wake_buf, .count = 1 }; + + if (spi_write(spi, &cfg, &wake) < 0 || spi_write(spi, &cfg, &wake) < 0) { + goto park; + } + + /* Select, send CMD0, read the reply. Response bytes are clocked with + * MOSI held high as the spec requires — spi_read() would drive zeros. */ + gpio_pin_set(cs_port, SD_CS_PIN, 0); + + const struct spi_buf cmd_buf = { .buf = (uint8_t *)cmd0, .len = sizeof(cmd0) }; + const struct spi_buf_set cmd = { .buffers = &cmd_buf, .count = 1 }; + + if (spi_write(spi, &cfg, &cmd) == 0) { + memset(tx, 0xFF, sizeof(tx)); + const struct spi_buf rtx_buf = { .buf = tx, .len = sizeof(rx) }; + const struct spi_buf_set rtx = { .buffers = &rtx_buf, .count = 1 }; + const struct spi_buf rrx_buf = { .buf = rx, .len = sizeof(rx) }; + const struct spi_buf_set rrx = { .buffers = &rrx_buf, .count = 1 }; + + if (spi_transceive(spi, &cfg, &rtx, &rrx) == 0) { + for (size_t i = 0; i < sizeof(rx); i++) { + if ((rx[i] & 0x80) == 0) { /* valid R1 token */ + LOG_INF("SD card present (R1=0x%02X) — " + "put into SPI idle", rx[i]); + goto release; + } + } + } + } + LOG_DBG("No SD card detected"); + +release: + /* Deselect, then one more byte so the card releases the bus. */ + gpio_pin_set(cs_port, SD_CS_PIN, 1); + memset(tx, 0xFF, sizeof(tx)); + const struct spi_buf rel_buf = { .buf = tx, .len = 1 }; + const struct spi_buf_set rel = { .buffers = &rel_buf, .count = 1 }; + + spi_write(spi, &cfg, &rel); + +park: + /* Restore the hog's parked state. */ + gpio_pin_configure(cs_port, SD_CS_PIN, GPIO_OUTPUT_HIGH); + return 0; +} + +SYS_INIT(m9_sd_quiesce, POST_KERNEL, 70); diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/README.md b/zephcore/boards/nrf52840/muziworks_r1neo/README.md index 7a98bc3..82935e8 100644 --- a/zephcore/boards/nrf52840/muziworks_r1neo/README.md +++ b/zephcore/boards/nrf52840/muziworks_r1neo/README.md @@ -4,13 +4,12 @@ A pocket LoRa node built on the **nRF52840 + SX1262** (RAK4630 stamp module on a custom muzi baseboard), with GNSS, buzzer, battery-backed RTC and a single user button. No display. -The port is cross-derived from three sources, because none of them agrees with -the others on every pin: Arduino MeshCore's +The port is cross-derived from several sources, because none of them agrees +with the others on every pin: Arduino MeshCore's [`variants/muziworks_r1_neo`](https://github.com/meshcore-dev/MeshCore/pull/2007), -Meshtastic's -[`variants/nrf52840/r1-neo`](https://github.com/meshtastic/firmware/blob/master/variants/nrf52840/r1-neo/variant.h), -and the vendor's own [product page](https://muzi.works/products/r1-neo-complete-meshtastic-device). -Where they conflict, the reasoning is recorded in the DTS comments and below. +other third-party firmware for the same board, and the vendor's own +documentation. Where they conflict, the reasoning is recorded in the DTS +comments and below. ## Build @@ -67,10 +66,10 @@ No QSPI external flash — contacts and channels live in internal flash. - **GPS TX/RX are swapped relative to the net names.** The schematic labels `UART_GPS_RX` (P0.24) and `UART_GPS_TX` (P0.25) from the *module's* point of - view, so the MCU's receive pin is P0.25. Meshtastic maps them that way; - Arduino MeshCore takes the labels literally and ends up listening on the - module's own input pin. ZephCore follows Meshtastic. Getting this backwards - is silent — the UART simply never sees a byte. + view, so the MCU's receive pin is P0.25. Arduino MeshCore takes the labels + literally and ends up listening on the module's own input pin; ZephCore uses + the corrected mapping. Getting this backwards is silent — the UART simply + never sees a byte. - **Soft power, not a switch.** The rail is gated by an I/O controller that only holds the DCDC up while P0.13 is asserted, and P0.29 tells it the MCU is alive (this also enables button and LED passthrough). Both are `gpio-hog` @@ -89,8 +88,8 @@ No QSPI external flash — contacts and channels live in internal flash. ## To verify on real hardware 1. **Battery multiplier.** `vbat-mv-multiplier = 6146` is a placeholder. The - two upstreams disagree on the divider: Arduino MeshCore's constant implies - ~1.73 (→ 6259, the RAK4631-family value), Meshtastic's variant.h states + sources disagree on the divider: Arduino MeshCore's constant implies + ~1.73 (→ 6259, the RAK4631-family value), the vendor documentation states 1.667 (→ 6031). Measure the cell with a DMM, compare against `get batt`, and set the value with ZephCore's convention: `multiplier = divider_ratio * 1.005 * 3600`. diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi index cf489d7..eb33a53 100644 --- a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi @@ -10,11 +10,10 @@ * Direction note: the schematic net names UART_GPS_RX (P0.24) and * UART_GPS_TX (P0.25) are labelled from the GPS module's point of * view, so UART_GPS_RX is the module's input and therefore the MCU's - * TX. Meshtastic's r1-neo variant.h maps them that way - * (PIN_SERIAL1_RX 25 / PIN_SERIAL1_TX 24); Arduino MeshCore's variant - * takes the net names literally and has them the other way round, - * which leaves the MCU listening on the module's own input pin — i.e. - * silence, and no fix, ever. Follow Meshtastic here. */ + * TX, and UART_GPS_TX is the MCU's RX. Arduino MeshCore's variant takes + * the net names literally and has them the other way round, which + * leaves the MCU listening on the module's own input pin — i.e. + * silence, and no fix, ever. The mapping below is the correct one. */ uart0_default: uart0_default { group1 { psels = ; diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts index 18c0bf4..868bd0d 100644 --- a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts @@ -60,9 +60,7 @@ user_button: button_0 { /* Active HIGH (pressed = VCC), no internal pull — the board * has an external pull-down and the line is passed through - * from the I/O controller. Matches Meshtastic's - * BUTTON_ACTIVE_LOW 0 / BUTTON_ACTIVE_PULLUP 0 / - * BUTTON_SENSE_TYPE INPUT_SENSE_HIGH. The polarity here is + * from the I/O controller. The polarity here is * what ui_prepare_for_system_off() reads to pick the * System OFF SENSE level, so it must stay correct. */ gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; @@ -122,10 +120,10 @@ * multiplier = divider_ratio * 1.005 * 3600 * (3.6 V full scale from ADC_GAIN_1_6 + ADC_REF_INTERNAL, +0.5% for * nRF SAADC gain error). The divider ratio itself is disputed between - * the two upstreams: Arduino MeshCore's constant implies ~1.73 - * (→ 6259, the RAK4631-family value), Meshtastic's r1-neo variant.h - * states 1.667 (→ 6031). 6146 splits them and is within ~2% of either; - * correct it once the board has been measured. */ + * sources: Arduino MeshCore's constant implies ~1.73 (→ 6259, the + * RAK4631-family value), while the vendor documentation states 1.667 + * (→ 6031). 6146 splits them and is within ~2% of either; correct it + * once the board has been measured. */ zephyr,user { io-channels = <&adc 7>; vbat-mv-multiplier = <6146>; @@ -292,8 +290,8 @@ reg = <0>; spi-max-frequency = <8000000>; - /* LoRa control pins. reset-gpios is wired and used — Meshtastic - * drives P1.06 as SX126X_RESET; Arduino MeshCore's RADIOLIB_NC is + /* LoRa control pins. reset-gpios is wired and used — P1.06 is + * the SX126x reset line; Arduino MeshCore's RADIOLIB_NC is * the outlier. ZephCore needs it: ui_prepare_for_system_off() * parks the radio in HW reset before System OFF. */ reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>;