Arduino due I2C slave holds SDA low following transmission of READ byte when that byte has MSB of 0

I raised this question last week on another forum. However since its specific to Arduino Due, I'm also now raising it here in the hope that someone has some experience of running a robust I2C slave on the Due.

I have an I2C slave running on an Arduino Due. The slave is working fine for all WRITE operations from the master, and also works correctly with many READ operations. However I am observing that when transmitting a byte value of less than 0x80 the SDA line is being held low by the SAM3x at the end of the byte transmission:

I2C READ operation scope screenshot, for byte value 0x08

As an I2C slave the SAM3x should be releasing the SDA line right after it has transmitted the final bit of the byte - i.e. when the SCL line goes low after the eighth SCL high during this byte transmission. This then frees the line for the master to pull it low to signal its "ACK" of the data. This is also vital for the subsequent STOP condition, during which the SDA line should transition high after SCL line has transitioned high. Since the master is the one that implements this STOP condition no slave should be holding SDA low except for the eight clock cycles where it is transmitting the READ byte value, and those ACK clock cycles following slave address and WRITE bytes.

For comparison, below is the waveform I'm seeing on our picoscope when the byte value being transmitted is > 0x80 (in this case the byte value is 0x88):

I2C READ operation scope screenshot, for byte value 0x88

I can see that in the case of the 0x88 transmission the SDA line is released immediately following the ACK clock cycle, thus neither master nor slave is holding it low. The master is then able to bring it low again such that it can implement the STOP condition. Releasing SDA to go high at this point is trivial, not required by the standard AFAIK. However it provides us the benefit that we can see that the slave must be holding the line low after - or maybe even during - the ACK bit clock cycle.

Note: I have verified that the SAM3x is the one holding the clock line low simply by resetting its I2C controller. As soon as we do that the SDA line is immediately released.

I have scoured the web looking for examples of I2C slave implementations on a SAM3x / Arduino Due and have found little that could aid in explaining what is happening here. Also the SAM3x datasheet (section 33.10) does not mention anything about this behaviour, as far as I could tell. In fact it rather states:

Until a STOP or REPEATED START condition is detected, TWI continues sending data loaded in the TWI_THR register

This indicates that it will release the SDA line after each complete byte (the full content of the TWI_THR register) has been shifted out - even if there is another byte in THR waiting to be sent. We know that the full byte has been shifted out, since we can see from the scope the exact same pattern for the lower nibble of the 0x08 byte as for the "working" byte value 0x88.

As we can see, the picoscope's I2C decoding is able to interpret both byte values, and presents both operations as being similarly successful. However, with the SDA line being held low, subsequent I2C operations then fail. Thus we need to find a solution to this - other than "only transmit bytes greater than 0x80"(!)

For completion, the I2C hardware initialisation code is below:

void Configure12cAsSlave(void) {

    PMC->PMC_PCER0  |= PMC_PCER0_PID23;      // TWI1 power ON
    PIOB->PIO_PDR   |= PIO_PDR_P13            // Enable peripheral control
                        | PIO_PDR_P12;
    PIOB->PIO_ABSR &= ~(PIO_PB12A_TWD1      // TWD1 & TWCK1 Peripherals type A
                        | PIO_PB13A_TWCK1);

    // I2C lines are Open drain by hardware, no need to program PIO_ODER

    TWI1->TWI_CR = TWI_CR_SWRST;  // TWIn software reset
    TWI1->TWI_RHR;                // Flush reception buffer

    TWI1->TWI_CR = TWI_CR_SVDIS | TWI_CR_MSDIS; // Disable Master and Slave modes

    // Enable slave mode
    TWI1->TWI_SMR = TWI_SMR_SADR(I2C_SLAVE_ADDRESS);
    TWI1->TWI_CR = TWI_CR_SVEN;     // Slave mode enable

    // Interrupt on Receive Ready (WRITE ops) and Transmit Ready (READ ops)
    TWI1->TWI_IER = TWI_IER_RXRDY | TWI_IER_TXRDY;
    NVIC_EnableIRQ(TWI1_IRQn);    
    NVIC_SetPriority(TWI1_IRQn, IRQ_PRIORITY__I2C); 
}

The interrupts that trigger the ISR are TWI_IER_RXRDY | TWI_IER_TXRDY. When TWI_IER_TXRDY (Transmit Hold Register ready) fires, the code simply writes a byte into TWI_THR as follows:

TWI1->TWI_THR = (uint32_t) iByteSent;

This byte has been previously loaded into TWI_THR during an earlier call to the ISR, following the end of a preceding write operation in which a register address has been provided to the slave. This means that THR is being written to twice during the same "SVACC" (Slave access) and could be interpreted then as having two bytes to transmit, rather than one. However this is necessary because on initialisation of the I2C controller as slave the TXRDY event fires continuously despite there being no actual I2C READ operation at the time, waiting for a byte to be written into the THR register. This blocks the Arduino from continuing until I write some dummy byte value into the THR register. However, that dummy byte then gets sent as the data byte value in the next (first) READ operation, and the byte value I provide in the TXRDY event handler then gets sent in the subsequent READ operation, and so on for subsequent READ operations. Thus every byte is delayed by one operation. In order to ensure the correct data value gets sent in all READ operations, I write to the THR register just as soon as I know what internal register address is being referred to - even before I know that a READ operation is about to happen (WRITE operations use the same format - except that the internal register address bytes are followed by another WRITE byte, thus my write to THR in those cases was redundant). This may seem convoluted but seemed necessary in getting the SAM3x working as an I2C slave. Given that 50% of byte values are being transmitted correctly in READ operations I trust that this "preloading" of the THR register is not the cause of the issues with transmitting bytes < 0x80 that I'm seeing.

On the Arduino Due I am using TWI1, via the two D20 and D21 pins that appear next to D19, as opposed to the two pins of the same numbers that appear above AREF, at the other end (top) of the headers along the right hand side. Note that although the code refers to this as "TWI1" (as apposed to TWI0), on the Arduino Due schematic its lines are referred to as SCL0-3 and SDA0-3 (as opposed to SCL1 and SDA1, which relate to TWI0). Thus the pins I am using for I2C include 1.5k pullups on the Arduino board itself. There are no other pullups on these lines. The I2C master is a third party system (an embedded custom FPGA), a black-box from my perspective.

Regarding the SAM3x holding TXRDY high right after initialisation of the I2C controller as slave: I wondered if this might be somehow related to other startup activities, thus moved the I2C slave initialisation into the main loop, to occur 1 second after the rest of the initialisation. This had no effect - TXRDY is still high immediately after I2C slave mode initialisation.

I will take a SWAG and say double check your I2C handling of the stop condition or repeated starts.

Thanks for your SWAG @gilshultz!

The I2C handling of the STOP condition is performed by the SAM3x I2C controller. On the SAM3x there is an interrupt that fires when a STOP condition is received - "EOSACC" (End of Slave Access); Enabling that interrupt means my C code (ISR) should get called in response to such an event. However given that the master is unable to generate the STOP condition (the SAM3x slave is holding one of the lines low) then the SAM3x never observes a STOP condition and instead behaves as though the "Slave Access" goes on indefinitely.

For example: If I enable "SVACC" interrupt ("SlaVe is currently being ACCessed") then the count of ISR calls instantly shoots into the millions: If each ISR call lasts only 1 microsecond (optimistic) then millions of ISR calls indicates that I2C operations are lasting seconds. Of course that's not realistic, certainly not the master's doing - that's all due to the Due holding the SDA line low, preventing the STOP condition. Same deal for repeated STARTs - these are not possible for the master to send due to the Due holding that line low.

Maybe I'm snow-blind by now. I think I'm missing your point?

Further info: As per the following thread comments I have also tried adding 100 ohm resisters in series between the master and slave on both SDA and SCK. This cleans up the noise on the lines quite a bit (good), but other than that I'm still seeing the SDA-held-low issue occurring as before.

https://forum.arduino.cc/index.php?topic=560415.0
reply #1

https://forum.arduino.cc/index.php?topic=488910.msg3691618#msg3691618
reply #8

Adding a brief update here to capture as much useful info as possible for future readers ...

I'm now using the Real Time Timer to reset the I2C controller 300 us after the last write operation. This is successful in releasing the SDA line maybe 75% of the time. (I do not yet know why its only sometimes successful)

I'm also seeing the SCK line being held low occasionally, following a READ operation. This is not due to clock stretching but rather that the processor has hard-faulted (verified using an LED that should blink on and off as the count of I2C ISR calls rises). I have yet to trace down the cause of that hard fault.

Another I will investigate are interrupt conflicts: On this Due we have an SPI peripheral interface, a simulated MUX and some other digital input pin interrupts. While their ISRs are all pretty quick (<= 2 ms) they nevertheless currently have a higher priority than the I2C slave's IRQ priority. I will work on a minimal sketch which reproduces the issue, which may help surface issues in my code.

(Still hopeful this is no hardware issue!)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.