RTL8720DN as I2C slave

Hi,

I’m currently trying to use the RTL8720dn chip as I2C Slave. I’ve tried using some functions and implementations of the sdk (i2c_api), but facing some weird issues where the polling mode doesn’t seem to work. Because of this, I’m curious if the Arduino package contains the full implementation. What’s the best practise in this occasion?

Currently I’m programming the chip with a library (self made) on top of the i2c_api.h header which in included in the arduino package (AmebaD/3.0.7/system/libameba/sdk/component/common/mbed/hal/i2c_api.h). But I just had a look into the SDK and faces the rtl8721d_i2c.h header and source (ambd_sdk/rtl8721d_i2c.c at master · ambiot/ambd_sdk · GitHub). Does any of you have experience with this case, and what’s best practise?
Use the i2c_api in the arduino package, or take a look into the official sdk. As told it’s required to use the 8720dn in polling mode (waiting for the master to send a request to respond on).

Thanks guys!

Kind regards,
Tim

1 Like

hi Tim,

afaik, arduino also only uses i2c in master mode

do you mind elaborate?

1 Like

Hi Simon,

That’s what I was a bit afraid of hahah! I tried editing the header a bit by changing the way it uses the slave. I did this by changing the TwoWireStatus and calling the functions, which are provided by the package a bit different. For writing, it seems to work. But it’s more of a brute force where the slaves tries to finish the transaction, which needs to be on the specific time when the master reads. Ofcourse, this isn’t the desired behaviour, but this was just me trying to find a workaround for the lack on slave-mode. Happy to hear that you think to know that the arduino package only provides master usage.

Have you used the rtl8721d_i2c header and source of the sdk? Just checked the code and header and this seems to have a chance of working. Only not sure if it provides a polling mode for the slave. In an ideal scenario, the slave keeps listening, and will be triggered (by callback) when he get’s polled by the master.
afbeelding

To elaborate on the polling issues:
The slave tries reading from the bus, but this doesn’t sees anything. However, i’ve checked the signal with a logic analyser, which is all fine (address and payload). I think, as you told, the arduino package spares some support for the slave side.

Hi Tim, you probably should look into this file instead, as this is the api primarily used for I2C operation

Hi Simon,

I could be wrong, but that’s the same as in the arduino package (AmebaD/3.0.7/system/libameba/sdk/component/common/mbed/hal/i2c_api.h), in which the slave mode seem to has lacks (as you told).

I can have a look in the source, but on the bus (using this api, as provided above) I’m not seeing a read signal when the slave is in polling mode.

check this line out, is this the one you need?

also this line for polling mode

1 Like

Hi everyone,

I’ve finally been able to get the slave in polling mode with the Master. However I’m stil facing a couple smaller issues.

The image below shows a simple I2C transmission where the master (raspberry pi 4, SMBUS protocol) sends a message (0). Then the interrupt of the slave is triggered which then returns WriteAddressed (i2c_slave_receive), with in this case the register (0x10 = 16 dec) and the payload (0). This is read correctly by the Slave, which then sends a response back (1) to the Master using i2c_slave_write. This flow is valid. Also on the logic analyzer (image below) you can see that the transaction is then completed, and the SDA line is pulled up by the Master.

Example

However, the i2c_slave_receive function remains constantly ‘ReadAddressed’ returned in an infinite loop. Is this because of the NACK to the (1) response message? Which on the one hand would be strange since the SDA line is pulled up, and the ‘1’ is actually read on the Master side. Does anyone have experience with this? And how do I fix this to repeat the previous transaction again without letting the WriteAddressed pass through because the slave thinks it is constantly being asked to read (which is not the case on the line).

The slave does not send the response until it receives a “ReadAddressed” for the first time. This also makes it strange that he keeps asking again while the master side (SMBUS) only asks for 1 byte.

Thanks guys.

1 Like

Hi,

I am resurrecting this thread because I am attempting to use the RTL8720DN as an I2C Slave, using the Wire library included in the Arduino SDK (AmebaD/3.0.8/libraries/Wire).

The problem I am facing is that the Wire.onReceive and Wire.onRequest callbacks never get called.
The receiver is not completely dead however, because it correctly ACKs the received bytes – until it has accumulated 16 bytes, after which it stretches the SCL clock forever (which should never happen in a Slave Receiver Transfer operation). This of course stalls the bus. Here are the last two bytes received before the receiver hangs:

Here is the test code I am using:

#include <string.h>
#include "Wire.h"

#define BLUE_LED            PA_11   // also known as PA13 in BW16 module (see variant.h)
#define I2C_ADDRESS         0x3a    // device address on the I2C bus

uint8_t I2C_SendBuf [] = { 41, 52, 63, 74 };
volatile uint8_t I2C_RecvBuf[32];
volatile bool I2C_newData;
volatile int I2C_bytes_rcvd;


void setup() {
    pinMode(BLUE_LED, OUTPUT);
    digitalWrite(BLUE_LED, 0);
  
    Serial1.begin(115200);
    while (!Serial1) ;
    
    Wire.begin(I2C_ADDRESS);        // declare the device as I2C slave
    Wire.onReceive(I2C_receiveEvent);
    Wire.onRequest(I2C_requestEvent);
    I2C_newData = false;
    I2C_bytes_rcvd = 0;

    digitalWrite(BLUE_LED, 1);
    delay(1000);
    digitalWrite(BLUE_LED, 0);
    printf("\nInit OK\n");
}


void loop() {
    if (I2C_newData) {
        printf("Received %d bytes, first = %02x\n", I2C_bytes_rcvd, I2C_RecvBuf[0]);
        I2C_newData = false;
    }
    delay(1);
}


static void I2C_receiveEvent(int howMany) {
    I2C_newData = true;

    digitalWrite(BLUE_LED, 1);
    for (int i = 0; i < howMany; i++) {
        I2C_RecvBuf[i] = Wire.read();
    }
    I2C_bytes_rcvd = howMany;
}


static void I2C_requestEvent() {

    digitalWrite(BLUE_LED, 1);
    Wire.write(I2C_SendBuf, sizeof(I2C_SendBuf));   // send response
}

Hence my simple question: am I missing something here, or is the Ameba Arduino SDK’s Wire library simply not able to perform as an I2C slave?

Thanks for help !

Hi xsutter,

Cool to see you’re also trying to use the 8720DN as i2c slave! I’ve to remember what I did to get it working, but the spoiler I can already give is that it does work as slave!

There were a couple thing I noticed during working with the chipset. I also never got it working with the Wire arduino library. It could be that they’re currently
supporting the correct pins, however at the time
i worked on it there were some issues there. My workaround have been creating a i2c-library yourself. This sounds like a lot of work, however you can look into the sdk code ameba provides on their ‘normal’ sdk. That code could almost identical be used and linked upon your arduino project. Just add the C or C++ (what you prefer) to your arduino files. Then you’re able to talk directly to the low-level i2c drivers from ameba, without the Wire lib in the middle. This will provide a lot of possibilities and insights in your I2C connection.

Next to that, if you’ll see clock stretching happening, you could conclude one of the next two things: your master pulls the clock down since It’s waiting for a transaction - which could be solved by implementing a timeout. Or there goes something wrong on slave side which affects the entire bus. Sadly, when you miss a single byte of I2C, you’re able to hang the entire bus. To fix all of this, you should implement a way in which a protocol (like go-back-n of retries) will save the day - without hanging the connection.

Sadly I’m not able and allowed to share my code, but I do hope this info will help you a bit.

Hi Tim,

Thanks for your suggestion ! Unless someone at Ameba gives us a fresh status of the current capabilities of the Wire library, I am afraid I will have to forget about it and dive deep into the Mbed API like you did.

As for clock stretching: this can never be an operation performed by the I2C master, only the slave can do this. Therefore the problem is with the Realtek as a slave – I hope the cause is just within the Wire library, and not something deeper.

Xavier

Hi Tim,
I am now switching to the Mbed i2c API. I would like to use interrupts as much as possible, however i2c_api appears to lack extensive IRQ management like we can find in spi_api or i2s_api, for example. I would like to avoid writing an i2c interrupt manager. Are you aware of someone, somewhere, who did the job already and that I could use as a starting point?

Thanks !

Hi everyone,
Hi Tim,

Having switched to the Mbed API, I am now in the situation you described, where upon a Read Request the RTL8720 slave is stuck in “ReadAddressed” mode. Recognizing the final NACK is normally the role of the RX_DONE interrupt flag. UM0400 - Ameba-D User Manual states that

“When the I2C is acting as a slave-transmitter, this bit is set to 1 if the master does not acknowledge a transmitted byte. This occurs on the last byte of the transmission, indicating that the transmission is done.”

My understanding is that the RTL8720 driver fails to correctly handle the RX_DONE interrupt flag. The only way I found to escape the “ReadAddressed” and get back to “NoData” is to disable and re-enable I2C using I2C_Cmd(). However this does not solve everything, because now the Tx FIFO is empty (which the RTL8720 does not like), and a subsequent Read Request stalls the I2C bus forever. Did you find a way to get out of this situation?

I also tried to have the I2C Master read one byte less than what I wrote in the Tx FIFO. UM0400 states that

“If the remote master is to receive n bytes from the I2C but the programmer wrote a number of bytes larger than n to the Tx FIFO, then when the slave finishes sending the requested n bytes, it clears the Tx FIFO and ignores any excess bytes.”

This is not the behaviour I have observed doing this. What I can see is that upon the final NACK, the i2c_slave_write() function never returns because the underlying I2C_SlaveWrite() waits forever for the Tx FIFO to be empty (the BIT_IC_STATUS_TFE flag). The reason of this clearly appears in the I2C_SlaveWrite() source code (have a look at the last while):

void I2C_SlaveWrite(I2C_TypeDef *I2Cx, u8* pBuf, u8 len)
{
	u8 cnt = 0;
	
	/* Check the parameters */
	assert_param(IS_I2C_ALL_PERIPH(I2Cx));

	for(cnt = 0; cnt < len; cnt++) {
		/* Check I2C RD Request flag */
		while((I2Cx->IC_RAW_INTR_STAT & BIT_IC_RAW_INTR_STAT_RD_REQ) == 0);

		if (I2C_SLAVEWRITE_PATCH) {
			I2Cx->IC_CLR_RD_REQ;
		}
		/* Check I2C TX FIFO status */
		while((I2C_CheckFlagState(I2Cx, BIT_IC_STATUS_TFNF)) == 0);
		
		I2Cx->IC_DATA_CMD = (*pBuf++);
	}
	while((I2C_CheckFlagState(I2Cx, BIT_IC_STATUS_TFE)) == 0);
}

This function never checks BIT_IC_RAW_INTR_STAT_RX_DONE, which is THE information it needs to return as soon as the Master terminates the transaction.

Does someone at Realtek have an opinion about all this? Don’t you think that RX_DONE management in this driver deserves a firmware update? If not, do you have a suggestion to help work around it and avoid freezing the I2C bus upon successive Read Requests?

Thanks for help!

1 Like

Hi xsutter,

Happy to hear you’ve made progress! This part of the RTL8720DN chip also took me a lot of time to figure out. Everything you’ve mentioned is right. I sadly can’t look into my code again due to NDA’s and secrets within the project. However I can remember that I cleared the Interrupt myself. It was a really ugly workaround since it should be handled on their own (build within the sdk), however as you mentioned, the slave will stay in that while loop.

Again, don’t pin me on this answer since I’m trying to remember everything from months ago haha.

There should be a clear_interrupt or i2c_clear function (something like that) which could clear the flag which is currently blocking your slave to read / write more. Whenever you’re in this blocking state, you should adapt a condition in which you can clear this interrupt which is stated in the SDK.

@xidameng maybe also knows some info on this point since I think we discussed this a while ago.

Hope something will help you!

Kind regards,
Tim

1 Like

Just found my notes from a while ago. Let me check if I can share anything.

For example, this is the way I created the ‘workaround’ for the slave getting stuck on the interrupt. I won’t win any contest with this code, since it’s not nice designed. However I couldn’t find a solution within the SDK which solved my problem.

void I2C_Client::handlePollingFromMaster()
{
    switch (slave_receive(((i2c_t *)m_pI2C)))
    {
    case ReadAddressed:
    {
        if (m_readTimeout > MAX_READ_TIMEOUT)
        {
            if (I2C_GetRawINT(((i2c_t *)m_pI2C)->I2Cx) & BIT_IC_RAW_INTR_STAT_RD_REQ)
            {
                I2C_ClearAllINT(((i2c_t *)m_pI2C)->I2Cx);
            }
        }
        m_readTimeout++;
        break;
    }
    case WriteAddressed:
    {
        char buff[1];
        if (i2c_slave_read(((i2c_t *)m_pI2C), buff, 1) > 0)
        {
            // Do your operation based on the Byte which is sent by the Master.
        }
        break;
    }
    case WriteGeneral:
    {
        break;
    }
    default:
    {
        break;
    }
    }
1 Like

Hi everyone,
Hi Tim,

Thanks for your example. Your idea of including a timeout helped me sort out the whole thing, and I think I have now a clear idea of what is happening and how to proceed. There are 3 things to take care about:

1- The main culprit is, as I suspected in my previous post, Ameba Low Level API’s I2C_SlaveWrite() function. This function fails to test the RX_DONE flag, hence cannot return upon the closing NACK.

I reimplemented the function and now use my own version. I just had to change the final

    while((I2C_CheckFlagState(I2Cx, BIT_IC_STATUS_TFE)) == 0);

for

    while(((I2C_CheckFlagState(I2Cx, BIT_IC_STATUS_TFE)) == 0) &&
          ((I2Cx->IC_RAW_INTR_STAT & BIT_IC_RAW_INTR_STAT_RX_DONE) == 0));

Now i2c_slave_write() does not block forever any more and returns correctly – even if the I2C master reads fewer bytes than what I sent to the Tx FIFO.

This is also the place where I included the timeout as per your idea, although I have to say that once I did things cleverly, it never triggered once. I just keep it as a precaution. Here is the version of the lines above including the timeout:

    int count = 0;
    while(((I2C_CheckFlagState(I2Cx, BIT_IC_STATUS_TFE)) == 0) &&
          ((I2Cx->IC_RAW_INTR_STAT & BIT_IC_RAW_INTR_STAT_RX_DONE) == 0) &&
	      (count++ < MAXCOUNT));

You have to use a logic analyzer to check the speed of this loop and determine a proper value for MAXCOUNT. On my setup every loop accounted for about 2µs (including the fact that I actually had to toggle a GPIO line inside the loop to give a signal to my analyzer), but your mileage may vary.

2- Once i2c_slave_write() returns, typically the slave software is still in a ReadAddressed switch case. Before breaking, you absolutely want to cleanup the status of the I2C device in order to allow the software to switch to the NoData case the next time. I tested two methods to do this:

    I2C_Cmd(I2Cx, DISABLE);
    I2C_Cmd(I2Cx, ENABLE);

which clears the Tx and Rx FIFOs, but leaves the interrupt flags untouched;
and

    I2C_ClearAllINT(I2Cx);

which just clears all interrupts.
Both methods give equally satisfactory results, although not performing the same operation. I don’t know why. Failing to use either of them keeps the system in the ReadAddressed state forever (which you described earlier in this thread). Should the master attempt to perform subsequent Write transactions, the slave would not be able to switch to WriteAddressed to read the received data. Eventually, once the 16-byte Rx FIFO is full, the I2C bus may hang.

3- You want to make sure that

    switch (slave_receive((i2c_t *)device))

is called often enough. Here the key parameter is the I2C master’s I2C timeout. Let T be the period at which you call slave_receive(). Upon a Read Request by the master, the RTL8720 performs an I2C clock stretch which lasts until the slave software actually sends bytes to the Tx FIFO. This clock stretch may last as long as T. Should the master’s I2C timeout elapse before T, what happens is that the master merely relinquishes the bus and abandons the transaction. Then, when the RTL8720 eventually has something to transmit, there is no clock any more on the bus to empty the Tx FIFO and complete the transaction.
Hence T should always be made shorter than the master’s I2C timeout by a good margin.

Now my system runs as smoothly as can be. Too bad the Ameba documentation is so sparse about how to use the I2C slave API successfully in the first place.
Hope that helps !

Xavier

1 Like

Thanks @xsutter for sharing the info :+1:

Hi Simon,
You’re welcome. To further refine my point, I think that the device cleanup I described in my 2- above should actually be performed inside the I2C_SlaveWrite() function right at the end, because anyway this action must be performed every time I2C_SlaveWrite() returns.

However I am not sure which is the appropriate cleanup action to perform. Is it a device DISABLE + ENABLE ? Is it an I2C_ClearAllINT()? Is it just an I2C_ClearINT(RD_REQ)? Probably only Realtek people aware of the detailed underlying mechanism could make a wise decision.

Anyway I think the I2C_SlaveWrite() function deserves an update in the next Ameba SDK release. This would make the life of I2C slave software developers considerably easier. Do you know what is the best way to request such an update?

Xavier