Skip to content

Software I2C: hold the data line until the clock is actually down - #261

Merged
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:soft_i2c_timing
Aug 19, 2026
Merged

Software I2C: hold the data line until the clock is actually down#261
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:soft_i2c_timing

Conversation

@ainyan03

Copy link
Copy Markdown
Contributor

The software I2C reached over the negative port numbers never received an
acknowledge from any device. The transfer looked well formed on a logic level —
start, eight bits, a released line for the ninth clock — but nothing answered,
so a bus with a device on it was indistinguishable from an empty one.

Cause

soft_i2c_write_byte() set the data line in the same breath as pulling the
clock low:

SOFT_I2C_LINE_LO(ctx.pin_scl);
if (data & 0x80) { SOFT_I2C_LINE_HI(ctx.pin_sda); } else { SOFT_I2C_LINE_LO(ctx.pin_sda); }

Both are register writes a few cycles apart. The data line therefore moves
while the clock can still read high, and a data change with the clock high is
a start or a stop to every device on the bus. The device sees the frame torn
up rather than a bit going by, and never gets as far as the address it was
being asked about.

Measured

An ES8311 on an ESP32-S3, probed at its own address:

Time between the clock going low and the data line moving Result
none no acknowledge
~130ns (a nop loop) no acknowledge
1µs acknowledge

The same address answers a plain bit-banged probe written alongside it, so the
device and the wiring were never in question.

Fix

Wait for each line to reach the level it was just given, instead of budgeting a
fixed time for it.

  • The clock is driven low, so waiting for it costs the fall time of the bus and
    nothing more: nothing on a light bus, more on a loaded one.
  • A released data line is given the time its own rise actually takes, which is
    the part that is slow and the part a fixed budget gets wrong on a loaded bus.

A fixed hold would have to come out of the setup time, and the setup time is
exactly what the slow rise of a released line needs once the clock rate goes
up. Measured with this fix in place, the probe answers at every setting the
port offers, including the fastest one where no fixed waits remain at all:

Half period Result
5µs (100kHz) acknowledge
1µs acknowledge
0 (no fixed wait) acknowledge

The same wait now precedes the start, the repeated start and the stop, where
the rise of the released data line decides whether the condition appears on the
bus at all — without it, a start could be issued while the line was still on
its way up, and a healthy bus could be mistaken for one that another device is
holding, which triggered the recovery clocks for no reason.

A line that never reaches its level is now reported, the way a clock that will
not rise already was, so a transfer ends instead of carrying on over a bus that
is not there. The recovery path is the one deliberate exception: a clock that
will not settle is the condition it exists to clear.

The acknowledge needs one distinction beyond that. This master drives the data
line low for the bit before it, so a low reading is either a device holding the
line or a rise that has not finished. A device holds it for the whole pulse
while a rise is over within the time the bus is allowed to take for one, so the
level is read a second time to separate the two. Without this, a port running
with no fixed waits reports an acknowledge from a bus that has no device on it.

Verified

  • ESP32-S3 hardware, six boards, through the caller that uses this port for
    board identification: every board reports the address that is actually on its
    bus, and none reports one that is not.
  • The bit-banged reference probe written next to it agrees with the library on
    every address on every board.
  • Builds green for ESP32 (Arduino) with the fragment compiled in.

Known limits, unchanged by this

  • init(), setPins() and release() do not take the slot lock and do not
    look at whether a transfer is open. That predates this change and is left
    alone here.
  • The second look at the acknowledge is bounded by the rise time the
    specification allows, not by a measurement of the bus in hand. A bus slower
    than the specification permits is still read as an acknowledge that is not
    there.

The software I2C moved SDA in the same breath as pulling SCL low. A data
line that changes while the clock still reads high is a start or a stop to
every device on the bus, so the transfer ended instead of carrying a bit
and no device ever acknowledged its address.

Wait for each line to reach the level it was just given, rather than
budgeting a fixed time for it: the clock is driven low, so waiting for it
costs the fall time of the bus and nothing more, and the released data
line is given the time its own rise actually takes. A fixed hold would
have to come out of the setup time, which is what the slow rise of a
released line needs at the higher clock rates.

The same wait now precedes the start, the repeated start and the stop,
where the rise of the released data line decides whether the condition
appears on the bus at all. A line that never reaches its level is
reported the way a clock that will not rise already was, so the transfer
ends instead of carrying on with a bus that is not there. The recovery
path is the one exception: a clock that will not settle is the condition
it exists to clear.

An acknowledge needs one more distinction. This master drives the data
line low for the bit before it, so a low reading is either a device
holding the line or a rise that has not finished. A device holds it for
the whole pulse while a rise is over within the time the bus is allowed
to take for one, so the level is read again to separate them.
@lovyan03
lovyan03 merged commit e39ca11 into m5stack:develop Aug 19, 2026
27 checks passed
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