mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-22 16:11:20 +00:00
M9 SD card settle
This commit is contained in:
1 parent
b2b7d0dc84
commit
47f24a0b1c
6 files changed
+178
-28
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/spi.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
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);
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 = <NRF_PSEL(UART_TX, 0, 24)>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in new issue
Block a user