Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/hardware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<X>`
to the header.
3. `platformio.ini`: a new `[env:<name>]` (board, partitions, `-DBOARD_<X>`,
Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
37 changes: 34 additions & 3 deletions src/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 17 additions & 3 deletions src/sys/I2cReg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Loading