diff --git a/docs/hardware.md b/docs/hardware.md index eb079f5..96d757a 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -926,6 +926,19 @@ performs; and BOOT + RST recovers any of them. Everything in `Config.h` is `#ifndef`-guarded, so the board header wins and anything it omits falls back to a sensible default. Host connectivity is **not** declared here — it comes from `boards.json` (next step). + I2C pins are the exception to "a sensible default": there is none. A panel + on I2C names its pair (`PIN_OLED_SDA`/`PIN_OLED_SCL`), a board with parts + on a general-purpose bus of its own names that pair (`PIN_I2C_SDA`/ + `PIN_I2C_SCL`), and a board with neither says nothing, which reads as -1: + no bus. Pins matter even where nothing sits on them, because the console's + `STATUS` and `I2C` scan the general bus and starting it takes those pins + over — the default used to be the T3-S3's 18/17, and on the Wireless Bridge + that is the radio's chip select and the PSRAM clock, which the first + `STATUS` after boot turned into a restart loop. The build now refuses a + general I2C pair (`PIN_I2C_SDA`/`PIN_I2C_SCL`) that lands on one of the + radio's pins, or on the PSRAM's chip select or clock on a classic ESP32 + with PSRAM; and with no pair named the bus is never started, so a driver + that asks for it is told it is down. 2. `src/Config.h`: one line in the board-selection block mapping `-DBOARD_` to the header. 3. `platformio.ini`: a new `[env:]` (board, partitions, `-DBOARD_`, diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 15b2378..b1a8772 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -39,7 +39,7 @@ ROM, and a power cycle boots whatever is in flash. | A node panics repeatedly and every reading taken afterwards looks healthy | the fault counters describe the *current* run. `diag.faults` / the `alloc_failures=` line are zeroed by the restart you are trying to explain, so a node that died of memory exhaustion answers `0` a minute later, every time — which is how one board panicked seven times in a week while every check said it was fine | read the **previous** run instead: `/api/status` gives `diag.boot.prev_alloc_failures` and `prev_contained`, and `STATUS` prints a `prev_uptime_s= prev_alloc_failures= prev_contained=` line on every node, always. Non-zero next to `clean=false` means it ran out of byte-addressable DRAM; zero means the panic was something else and the coredump is next. The word **`unknown`** rather than a number means nothing survived to report — a power cut, not a crash — or, where `prev_uptime_s` is a real figure beside it, the first boot after upgrading from a firmware that did not keep the counts. `prev_contained` counts every exception the run caught, not only allocation failures, so on its own it is not evidence of memory pressure. `tools/soak.py` records both and names the dead run in its summary | | A task's stack needs sizing, or a node restarts with `reason="panic or unhandled exception"` under load | stacks are guessed at when a task is written and only measurement settles them | `STACKS` reports bytes never used, per task, and names the tightest. The figures are high-water marks, so they only fall — read them after days of real traffic, not off the bench, and leave margin: they are the worst seen, not the worst possible | | A device answers the I2C scan but you do not know what it is, or a driver written from a vendor page finds the wrong chip id | the page describes a different revision of the board, or the part is not the one named | `I2C 0x7c` dumps that device's first 16 registers, which is where these parts keep their chip id and revision. Compare against the datasheet before writing a driver: a driver built on a page that is wrong fails in a way that looks like wiring. Sixteen copies of one byte mean the part does not auto-increment its register pointer, not that the bus is broken | -| A part on I2C reads as absent — touch layer, keyboard, charger — and the wiring looks right | its driver probed once at boot, before the part had finished coming up; or the board header names the wrong pins, which is indistinguishable from a part that is not fitted | `I2C` scans every bus now and says what answered: `RM I2C i2c main sda=7 scl=6 parts=3 acked=0x51 0x6b 0x7c`. An address that answers here but not to its own driver is a timing or address fault, not wiring; `parts=0` on a bus with parts on it is the pins. `STATUS` carries the same lines from the last scan | +| A part on I2C reads as absent — touch layer, keyboard, charger — and the wiring looks right | its driver probed once at boot, before the part had finished coming up; or the board header names the wrong pins, which is indistinguishable from a part that is not fitted | `I2C` scans every bus now and says what answered: `RM I2C i2c main sda=7 scl=6 parts=3 acked=0x51 0x6b 0x7c`. An address that answers here but not to its own driver is a timing or address fault, not wiring; `parts=0` on a bus with parts on it is the pins. `STATUS` carries the same lines from the last scan. A board with no general I2C bus — the Wireless Bridge, the Wireless Paper — prints no `i2c main` line at all, rather than a scan of pins it uses for something else | | Web flasher needs BOOT held | previous firmware's USB stack ignored the reset sequence | one-time; RetiMesh firmware resets hands-free | | `pio run -t upload` says `several ESP32 ports` | more than one board attached and no port chosen | `--upload-port /dev/serial/by-id/…` (by-path for CP2102 boards, which all report serial `0001`) | | `pio run -t upload` says the port `did not reappear` | the node accepted the bootloader request but its USB device did not come back within 8 s — a hub or host that re-enumerates slowly | hold BOOT, press RST, run the upload again; `RETIMESH_NO_AUTO_BOOTLOADER=1` leaves the reset to esptool | diff --git a/src/Config.h b/src/Config.h index 9b306c2..66559b1 100644 --- a/src/Config.h +++ b/src/Config.h @@ -644,7 +644,7 @@ #define SD_LOG_MAX_BYTES (1024UL * 1024UL) // --------------------------------------------------------------------------- -// Display — SSD1306 128x64 on I2C by default (T3-S3: SDA 18 / SCL 17, 0x3C) +// Display — SSD1306 128x64 on I2C by default (0x3C; the board names the pins) // --------------------------------------------------------------------------- #ifndef HAS_DISPLAY #define HAS_DISPLAY 1 @@ -681,11 +681,23 @@ #ifndef OLED_CONTROLLER #define OLED_CONTROLLER OLED_CONTROLLER_SSD1306 #endif +// The panel's pair, which a board with an OLED names in its own header. There +// is no default pair, and -1 is what "no such bus" reads as — I2cReg::mainBus() +// does not start a bus on pin -1 and hosts() does not list one. The default used to be 18/17, the T3-S3's +// pair, and the two boards that name none inherited it: the Wireless Bridge, +// which has no panel, and the Wireless Paper, whose panel is on SPI. Through +// PIN_I2C_* below that handed each a general bus on pins it uses for something +// else, and the first STATUS after boot scans that bus — on the Bridge, GPIO 18 +// is the radio's chip select and GPIO 17 the D0WDQ6's PSRAM clock, and the scan +// panicked the node into a restart loop that came out of it with no PSRAM. #ifndef PIN_OLED_SDA - #define PIN_OLED_SDA 18 + #define PIN_OLED_SDA -1 #endif #ifndef PIN_OLED_SCL - #define PIN_OLED_SCL 17 + #define PIN_OLED_SCL -1 +#endif +#if HAS_DISPLAY && DISPLAY_KIND == DISPLAY_KIND_OLED && (PIN_OLED_SDA < 0 || PIN_OLED_SCL < 0) + #error "an OLED board must name its panel's pins (PIN_OLED_SDA/PIN_OLED_SCL) in its header; a board with no panel sets HAS_DISPLAY 0" #endif // The board's general-purpose I2C — the bus that carries whatever is not the // panel: a charger, an accelerometer, a touch controller, a keyboard. Most @@ -702,6 +714,25 @@ #ifndef PIN_I2C_SCL #define PIN_I2C_SCL PIN_OLED_SCL #endif +// Nothing may start this bus on a pin the board has already given to something +// that dies when it is clocked: the radio's SPI and control lines, and on a +// classic ESP32 with PSRAM the PSRAM's chip select and clock — GPIO 16 and 17 +// on the D0WD parts, read from the core's sdkconfig rather than restated here. +// Wire.begin() takes the pins over from whatever held them, so a clash is not +// a bus with nothing on it; it is the radio or the heap failing underneath a +// console command. Refused here, so a header that clashes does not compile. +#define RM_PIN_IS_RADIO(p) ((p) >= 0 && ((p) == PIN_LORA_SCK || (p) == PIN_LORA_MISO || \ + (p) == PIN_LORA_MOSI || (p) == PIN_LORA_CS || (p) == PIN_LORA_RST || \ + (p) == PIN_LORA_DIO0 || (p) == PIN_LORA_DIO1 || (p) == PIN_LORA_BUSY)) +#if RM_PIN_IS_RADIO(PIN_I2C_SDA) || RM_PIN_IS_RADIO(PIN_I2C_SCL) + #error "PIN_I2C_SDA/PIN_I2C_SCL name one of the radio's pins; a board with no general I2C bus leaves them -1" +#endif +#if defined(CONFIG_IDF_TARGET_ESP32) && defined(BOARD_HAS_PSRAM) && defined(CONFIG_D0WD_PSRAM_CLK_IO) && \ + (PIN_I2C_SDA == CONFIG_D0WD_PSRAM_CS_IO || PIN_I2C_SDA == CONFIG_D0WD_PSRAM_CLK_IO || \ + PIN_I2C_SCL == CONFIG_D0WD_PSRAM_CS_IO || PIN_I2C_SCL == CONFIG_D0WD_PSRAM_CLK_IO) + #error "PIN_I2C_SDA/PIN_I2C_SCL name the PSRAM's chip select or clock (CONFIG_D0WD_PSRAM_CS_IO/CLK_IO)" +#endif +#undef RM_PIN_IS_RADIO // Panels on a switched rail need it brought up before they are probed, and it // is active low on every board that has one so far. #ifndef HAS_DISPLAY_VEXT diff --git a/src/sys/I2cReg.h b/src/sys/I2cReg.h index 4835c91..3e2f62d 100644 --- a/src/sys/I2cReg.h +++ b/src/sys/I2cReg.h @@ -23,6 +23,7 @@ namespace I2cReg { // and none would agree about the pins. namespace detail { inline bool& mainStarted() { static bool v = false; return v; } +inline bool& mainUp() { static bool v = false; return v; } inline bool& h1Started() { static bool v = false; return v; } inline int& h1Sda() { static int v = -1; return v; } inline int& h1Scl() { static int v = -1; return v; } @@ -37,9 +38,21 @@ inline int& h1Scl() { static int v = -1; return v; } // and its keyboard on this one: two drivers, neither of which owns the bus, // both of which need it up, and each initialised at a different point in // setup(). Whoever asks first brings it up; everyone else gets the same bus. +// +// A board that names no general pair has no such bus, and this is the one +// place that holds to it. Wire.begin(-1, -1) does not mean "no pins" to the +// core: TwoWire::initPins() swaps a negative pin for the variant's SDA/SCL, +// which on the Wireless Bridge is 21/22 — its Vext and its LoRa LED. So the +// call is not made at all, and the Wire object left unstarted refuses every +// transaction rather than clocking someone else's pins. inline TwoWire& mainBus() { if (!detail::mainStarted()) { - Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, I2C_HZ); +#if PIN_I2C_SDA >= 0 && PIN_I2C_SCL >= 0 + detail::mainUp() = Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, I2C_HZ); +#else + log_e("I2C: this board names no general pair (PIN_I2C_SDA/PIN_I2C_SCL), so that bus is not started"); + detail::mainUp() = false; +#endif detail::mainStarted() = true; } return Wire; @@ -59,8 +72,9 @@ inline TwoWire& mainBus() { // before it decides its part is missing. inline TwoWire& busFor(int sda, int scl, uint32_t hz, bool* up = nullptr) { if (sda == PIN_I2C_SDA && scl == PIN_I2C_SCL) { - if (up) *up = true; - return mainBus(); + TwoWire& bus = mainBus(); + if (up) *up = detail::mainUp(); + return bus; } if (!detail::h1Started()) { detail::h1Started() = Wire1.begin(sda, scl, hz);