Skip to content

Board detection: probe the address instead of the pull-up (#330) - #331

Merged
lovyan03 merged 7 commits into
m5stack:developfrom
ainyan03:board_probe_i2c
Aug 19, 2026
Merged

Board detection: probe the address instead of the pull-up (#330)#331
lovyan03 merged 7 commits into
m5stack:developfrom
ainyan03:board_probe_i2c

Conversation

@ainyan03

@ainyan03 ainyan03 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Fixes the Atom VoiceS3R being reported as an AtomS3R-Ext (#330).

Builds on the software I2C fix in M5GFX (m5stack/M5GFX#261), which is what the
probe here now speaks through, and wants the pin restore fix alongside it
(m5stack/M5GFX#262).

Cause

_detect_i2c_device() was not I2C. It made no start condition, clocked every
address bit twice across twenty pulses, and read the ninth bit a millisecond
after the clock had gone back down — long after any device that acknowledged
had released the line. On a bus with pull-ups the reading is high whether or
not the address is there, so what the function returned was the presence of a
pull-up on the pins, and the five call sites read that value as the device
being present.

Every board on this branch has pull-ups on its internal bus, so the first
condition matched on all of them. The chain checks the AtomS3R-Ext BMI270
first, so the Atom VoiceS3R — same package, same internal bus pins, an ES8311
where the other has a BMI270 — came out as an AtomS3R-Ext. That is why none of
its examples work: only board 145 gets the I2S pins and the amplifier callback
for its built-in audio.

The report's workaround of calling M5.begin() a second time with
fallback_board cannot help: begin() returns immediately once the board is
known, and fallback_board only applies when detection came back unknown.

Change

Probe the address, over the software I2C port. Open drain, no peripheral
touched, and the acknowledge that actually comes back. The return value now
means what the call sites always read it as.

Pins that carry no pulled-up bus are left alone, using the same test the
display autodetection applies to pins that may not be I2C at all. The one bus
that gets a second chance is the one holding its data line down — the clock
comes back against the internal pull-down while the data line stays low — which
is a device interrupted mid transfer rather than a pin without a pull-up. That
one is offered a stop condition and looked at again; this file already records
a device that needed exactly that (UnitHEART MAX30100, in the Capsule branch).
The stop is made by driving low and releasing, never by driving high, and the
lines are parked low before the pins become outputs, so no high is ever driven
into whatever is on the other side.

Decide the S3PICO boards on their internal bus first. The three boards
sharing that bus were separated by a probe on another pin pair in between:
BMI270, then the Stamp-S3Bat address on 48 and 47, then the ES8311. The Atom
VoiceS3R drives its speaker out of GPIO48, so a board fully identified by its
own bus was reaching for the I2S data line before it got there. Within the bus
the BMI270 is asked first: the AtomS3R-Ext and the AtomS3RCam are the same
board, and on the Ext the camera end is a small breadboard the owner is free to
wire up, so an address that is not the BMI270 can appear on the internal bus of
a board that is one. The part that is always fitted decides.

Wait once, not once per address. Every probe waited 50ms before touching
the pins. The old one always answered on the first address, so that wait was
paid once; an address that now says it is not there would have made it four
times over on a board with nothing on its internal bus. The wait is about how
long ago the board was powered, so it belongs at the entrance to the check.

The function is renamed to _probe_i2c_addr(); M5GFX has a function of the old
name with a different contract, and the two are easy to confuse.

Measured on hardware

Each board was read three ways before M5.begin(): a plain bit-banged scan of
the bus, the new probe, and then M5.getBoard().

Board On the internal bus Expected Reported
Atom VoiceS3R 0x18 only (ES8311) 145 145
AtomS3R-Ext 0x68 only (BMI270) 143 143
AtomS3RCam 0x68, camera 0x21 (GC0308) 144 144
AtomS3RCam M12 0x68, camera 0x3C (OV3660) 144 144
StampS3Mini a pull-up on one line of the pair, nothing on the other 155 155
M5PowerHub 0x50 on 45/48 146 146

A full scan of the Atom VoiceS3R internal bus, 0x08 through 0x77, answers
at 0x18 and nowhere else — no inertial sensor on that bus on this unit. Both
AtomS3RCam variants still reach the camera identification and come out as 144.

One probe went from 49.8ms to 0.31ms on an AtomS3RCam, with the 50ms now spent
once for the whole identification.

Boards outside this branch were run as well, to see that the pin restore fix
underneath does not disturb display autodetection: M5StopWatch (30, 468x468),
Core2 v1.1 (2, 320x240) and PaperS3 (19, 540x960) all come up and keep running.

Not verified on hardware

Stamp-S3Bat. No unit at hand. This branch changes what its detection
depends on: the old code accepted the pull-ups on GPIO 48/47 and never asked
the M5PM1 at 0x6E for an answer, while this one requires the acknowledge. A
device slow to come up could be missed where it used to be assumed.

Known, and deliberately left for later

false from the probe means "no acknowledge", which covers the address not
being there, the pins carrying no pulled-up bus, and the port failing to open.
The chain reads all of them as absent, so a bus that could not be probed ends
at the last else rather than at unknown. Splitting the answer three ways is
the right shape, and is worth doing on its own rather than inside this fix.

The internal_imu = false line for the Atom VoiceS3R suggests the board may
carry an inertial sensor that the unit measured here does not have. The
detection order above is chosen so that either answer identifies the board
correctly.

The helper that decides which board this is did not speak I2C. It made no
start condition, clocked every address bit twice over twenty pulses, and
read the ninth bit a millisecond after the clock had gone back down, when
any device that acknowledged had long released the line. What came back
was the pull-up on the pins, not an answer from the address, and the
caller read that value as the device being present.

Every board on the branch has pull-ups on its internal bus, so the first
condition matched on all of them. AtomS3RExt is checked first and Atom
VoiceS3R, which carries an ES8311 where the other carries a BMI270, was
reported as AtomS3RExt.

Send the address over the software I2C port instead, which drives the
lines open drain, keeps away from the peripheral, and reports the
acknowledge it actually receives. The return value now means what the
five call sites always read it as. A stop is offered first, because a
device left mid transfer needs one to let go and one board in this same
file already records a device that needed exactly that; it is made by
driving low and releasing, never by driving high, so a device holding a
line is not fought over. Pins that carry no pulled up bus are left alone,
the same test the display autodetection uses on pins that may not be I2C
at all. A single retry covers a device that is slower to answer than the
old test, which never waited for an answer in the first place.

The name says what it does now: the same name in M5GFX belongs to a
different function with a different contract.
The three boards that share the internal bus were separated by a probe on
another pair of pins in between: BMI270, then the Stamp-S3Bat address on
48 and 47, then the ES8311. Atom VoiceS3R drives its speaker out of
GPIO48, so a board fully identified by its own bus reached for the I2S
data line before it got there. Take the internal bus first and only step
off it when nothing there answered.

Within that bus the BMI270 is asked first. The AtomS3R-Ext and the
AtomS3RCam are the same board, and on the Ext the camera end is a small
breadboard the owner is free to wire up, so an address that is not the
BMI270 can appear on the internal bus of a board that is one. The sensor
that is always fitted decides, and something added later cannot take the
identity away.

Measured on hardware: Atom VoiceS3R answers at 0x18 alone, AtomS3RExt and
both AtomS3RCam variants at 0x68 alone, and StampS3Mini has a pull-up on
one line of the pair and nothing on the other.
The probe held the pins low nine times over before it had established that
they carry an I2C bus at all: the stop preamble ran first, and only then
came the test for a pull-up. These pins are how the board is identified,
so until that test passes they may be anything. Run the test first and
offer the stop only once the pins have answered for themselves.

Take the second software I2C slot rather than the one the display
autodetection uses. The slots carry no ownership - opening one takes over
whatever settings were there - so two libraries sharing a slot rests on
nothing but the order they happen to run in.

Drop the retry. Nothing between the two attempts changes the state of the
device being asked: no reset, no power, no clock. It doubled the time
spent on every address that is not answering, which the boards with
nothing on their internal bus pay three times over.
library.json and idf_component.yml already ask for it; the Arduino
manifest asked for M5GFX with no version at all. An older one has no
software I2C behind the negative port numbers, and the port number
reaches the hardware port table as an index instead.
Checking for the pull-up before offering a stop condition left the one bus
that most needs the stop unable to get it. A device interrupted mid read
holds the data line down, and a software reset does not take its power
away, so the line is still held when the board is identified again. The
check reads that as no bus and returns before the stop is ever made.

The signature is specific enough to act on: the clock comes back high
against the internal pull-down while the data line stays low. That is a
held bus, not a pin without a pull-up, where neither line comes back.
Offer the stop in that case and look again; every other answer returns
as it did.
Every probe waited 50ms before it touched the pins. The old probe always
answered on the first address, so that wait was paid once; now that an
address which is not there says so, it is paid again for every address
that misses - four times over on a board that answers nothing on its
internal bus.

The wait is about how long ago the board was powered, not about the
address being asked, so it belongs at the entrance to the board check.
Measured on an AtomS3RCam: one probe went from 49.8ms to 0.3ms, and the
50ms is now spent once for the whole identification.
The bus test set both pins to output while their latches were still high
from the pull-up mode before it, so each pin drove a push-pull high for
the moment before the test pulled it low. On pins that may not be an I2C
bus at all - which is the whole point of the test - that is a driven high
into whatever is on the other side.

Put the latch low first. The lines are still driven low by the test after
that, but a high is never driven at all.
@lovyan03
lovyan03 merged commit 57e8ea2 into m5stack:develop Aug 19, 2026
27 checks passed
@ainyan03
ainyan03 deleted the board_probe_i2c branch August 19, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants