Skip to content

esp32: I2C.Tx discards address NACK on reads #5584

Description

@pottekkat

On ESP32, machine.I2C0.Tx(addr, nil, buf) returns nil for every address, even when nothing is on the I2C bus. The hardware detects the address NACK, but the && !readLast check at

case mask&esp.I2C_INT_STATUS_ACK_ERR_INT_ST_Msk != 0 && !readLast:
discards it.

I traced the issue, and this is the root cause:

  1. The address byte of a read is sent by an i2cCMD_WRITE command, emitted at

    case i2cCMD_READ:
    if needAddress {
    needAddress = false
    i2c.Bus.SetDATA_FIFO_RDATA((uint32(addr)&0x7f)<<1 | 1)
    i2c.Bus.SLAVE_ADDR.Set(uint32(addr))
    reg.Set(i2cCMD_WRITE | 1)
    reg = nextAddress(reg)
    }

  2. i2cCMD_WRITE is the only constant that sets ack_check_en

    const (
    i2cCMD_RSTART i2cCommandType = 0 << 11
    i2cCMD_WRITE i2cCommandType = 1<<11 | 1<<8 // WRITE + ack_check_en
    i2cCMD_READ i2cCommandType = 2 << 11
    i2cCMD_READLAST i2cCommandType = 2<<11 | 1<<10 // READ + NACK
    i2cCMD_STOP i2cCommandType = 3 << 11
    i2cCMD_END i2cCommandType = 4 << 11
    )
    so it is the only command that can set ACK_ERR.

  3. i2cCMD_READLAST sets ack_value, which instead sends a NACK. ACK_ERR is in the other direction. In TRM section 21.3.7, it says:

    I2C_ACK_ERR_INT: Triggered when the Master receives an ACK that is not as expected, or when the Slave receives an ACK whose value is 1.

  4. The readLast flag is set when the command list is built

    if split {
    readLast = true
    reg.Set(i2cCMD_READLAST | 1)
    reg = nextAddress(reg)
    readTo = c.data[c.head : c.head+bytes+1] // read bytes + 1 last byte
    cmdIdx++
    } else {
    and not when the error is checked. A read of 32 bytes or fewer fits in the controller's 32-byte RAM, and the whole transaction is run in one go. When the status register is checked, readLast is already true, and the address error is dropped.

Reads of 33 bytes or more won't have this issue because they are split with i2cCMD_END and the part carrying the address would be checked before the readLast flag is set.

Steps to Reproduce

Wire any I2C device to an ESP32 and probe every available address with a read:

for addr := uint16(0x08); addr <= 0x77; addr++ {
	buf := make([]byte, 1)
	if err := machine.I2C0.Tx(addr, nil, buf); err != nil {
		continue
	}
	println("found", addr)
}

The expectation is that only the addresses that are on the bus would be "found." But actually, all 112 are reported as present.

If you probe with a write instead, Tx(addr, []byte{0x00}, nil), it correctly reports the devices that are present on the bus. A write does not emit a READLAST, so the readLast flag stays false.

Environment

  • TinyGo 0.41.1 and dev at c421943
  • ESP32-D0WD-V3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions