Author Topic: Emulate an I2C EEPROM with an AVR?  (Read 3100 times)

0 Members and 1 Guest are viewing this topic.

Online HwAoRrDkTopic starter

  • Super Contributor
  • ***
  • Posts: 1551
  • Country: gb
Emulate an I2C EEPROM with an AVR?
« on: July 31, 2022, 07:05:19 pm »
I'm wondering whether it's possible to emulate an I2C EEPROM using an AVR (specifically, a newer 1-series ATtiny) acting as an I2C slave.

The specific application I have in mind is in the creation of a Raspberry Pi HAT board. With those, you have the option to include on the HAT board a small EEPROM connected to a dedicated pair of I2C pins on the Pi's GPIO header, that will contain identifying info about the Hat, as well as Linux kernel device tree overlay data. The Pi will probe for and read the contents of the EEPROM at boot time, and configure itself accordingly. The EEPROM access is read-only, and at a fixed slave address (0x50).

The Hat I'm considering designing will already have an ATtiny MCU on it that has an I2C peripheral, and because the I2C EEPROM protocol seems fairly simple (e.g. CAT24C32 datasheet, pg. 8), I wonder if I can emulate one on the AVR. The amount of data is quite small (probably a few hundred bytes at most) and is read-only, so I can easily compile it in to flash on the MCU.

There are however some requirements for the EEPROM in the Raspberry Pi Hat design guidelines:

Quote
- 24Cxx type 3.3V I2C EEPROM must be used (some types are 5V only, do not use these).
- The EEPROM must be of the 16-bit addressable type (do not use ones with 8-bit addressing)
- Do not use 'paged' type EEPROMs where the I2C lower address bit(s) select the EEPROM page.
- Only required to support 100kHz I2C mode.
- Devices that perform I2C clock stretching are not supported.
- Write protect pin must be supported and protect the entire device memory.

The highlighted 5th requirement is the only potential sticking point I have concerns about. I suppose one potential area for clock stretching to happen when emulating is if the AVR is not fast enough fetching/preparing the data to be read in-between when the 16-bit read address has been transmitted by the master and the master expecting to read the data byte(s). Some brief research indicates it might be possible to handle the EEPROM reads without clock stretching, as there is an Atmel app note AVR290 giving an example implementation of an I2C slave giving "no clock stretching – true 100kHz operation".

Has anybody done this before? Or have any thoughts?
 

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 6142
  • Country: es
Re: Emulate an I2C EEPROM with an AVR?
« Reply #1 on: August 01, 2022, 02:05:55 pm »
Just setup the AVR as slave I2C.
Make state machine, checking for start/stop conditions, the protocol is very simple.
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 

Offline hans

  • Super Contributor
  • ***
  • Posts: 1665
  • Country: nl
Re: Emulate an I2C EEPROM with an AVR?
« Reply #2 on: August 01, 2022, 02:28:17 pm »
I've implemented a generic I2c slave stack in C++, of which one of the function classes is EEPROM. I don't use clock stretching for similar reasons.

However what I did was push the received data buffer to some statemachine that processes and executes the command. If it's a write, then it waits till the command is finished. If it's a read, it tries to put data into a transmit buffer as quick as it can.

While the command is still being processed, the slave device will NAK any new incoming I2c start+address headers. This is to prevent corruption of the received command buffer, but also as backpressure to the host to indicate the device is still busy. The host could poll the slave device till it's finished or some timeout value.
 

Online HwAoRrDkTopic starter

  • Super Contributor
  • ***
  • Posts: 1551
  • Country: gb
Re: Emulate an I2C EEPROM with an AVR?
« Reply #3 on: August 01, 2022, 05:09:08 pm »
I've been attempting to figure out what the timings will be for processing data writes (i.e. EEPROM address) and responding to reads, to see how much time the microcontroller will have.

From what I can see (from aforementioned 24C32 datasheet), there are three situations that need to be handled for EEPROM reads:

- Immediate read: slave responds with a single byte of data from wherever the internal address pointer is.
- Selective read: master writes two bytes giving a 16-bit address, then slave responds with a single byte of data from that address.
- Sequential read: slave responds with a single byte of data from wherever the internal address pointer is. If the master ACKs, respond with the next byte. This can continue indefinitely until the master NACKs indicating it doesn't want any more data.

To me, an immediate read and sequential read seem to be essentially the same logic - an immediate read can be considered just a single-byte sequential read. So basically I'm just dealing with an addressed read of one data byte or non-addressed read of one or more data bytes. And even an addressed read isn't really a separate case either, as there's a repeated start after the address, so the latter part of that is essentially another non-addressed read. If that makes sense! :)

As far as timings are concerned, I think if I am to avoid clock-stretching, it basically boils down to how long the ACK period is between receiving either the slave address or a data byte. That is just one SCL clock cycle. If the Raspberry Pi is using 100kHz, then that'll be 10us. Assuming I'm running the ATtiny at the default 20MHz, I'm getting 20 CPU cycles per microsecond, so I'll have 200 cycles total to do my processing. That seems like it might be enough. I suppose a lot will depend on the interrupt latency of the ATtiny 1-series - I don't know how many cycles it is. Any ideas?

However what I did was push the received data buffer to some statemachine that processes and executes the command.

Yes, I think a simple state machine will indeed be the best way to handle things. I'm thinking I'll need to handle the following states within the I2C interrupt handler each time it's called.

Start:
If APIF flag of SSTATUS register set, matching slave address received; automatically ACK-ed by hardware. Check DIR flag in SSTATUS register to see if it's a write. If so, set state to 'Address MSB'.

Address MSB:
If DIF flag of SSTATUS register set, read SDATA register to get address MSB and store. With ATtiny's 'Smart Mode', ACK is automatic upon reading SDATA. Set state to 'Address LSB'.

Address LSB:
Same as above, but for LSB of address. Set state to 'Begin Data'.

Begin Data:
If APIF flag of SSTATUS register set, matching slave address received; automatically ACK-ed by hardware. Check DIR flag to see if it's a read. If so, set state to 'Transmit Data'.

Transmit Data:
If DIF flag of SSTATUS register set, read a data byte from EEPROM contents in flash at previously stored address, increment the address pointer, and transmit data by writing byte to SDATA register. Set state to 'Transmit More Data'.

Transmit More Data:
If DIF flag of SSTATUS register set, interrupt was fired on completion of previous transmission, check RXACK flag in SSTATUS register - if ACK, set state to 'Transmit More Data'; if NACK, we're done, set state to 'Start'.

Am I missing anything here?

Hopefully the code to handle each state will be able to be executed in the amount of time required before the next SCL cycle, so clock stretching won't happen.

While the command is still being processed, the slave device will NAK any new incoming I2c start+address headers. This is to prevent corruption of the received command buffer, but also as backpressure to the host to indicate the device is still busy. The host could poll the slave device till it's finished or some timeout value.

Hmm, I have no idea whether the Raspberry Pi would handle NACK-ing in response - that is, whether it would try again. So I don't know if this would be a feasible strategy.
 

Offline cv007

  • Frequent Contributor
  • **
  • Posts: 841
Re: Emulate an I2C EEPROM with an AVR?
« Reply #4 on: August 01, 2022, 07:51:20 pm »
My ATtiny3217 Curiosity Nano 'hat'. That does nothing.

I have a Pi model B haven't used in a long time. Put a fresh os on sd card, setup for headless. Connected dslogic to pins27/28 of header. Create a simple twi slave project on a tiny3217 curisosity nano and connected to i2c bus. Booted Pi, watched/captured i2c bus, looked at hat info in file system. Works ok.

Here is my twi library that was used (twis.h, twis.c, twiPins.h, MyAvr.h)-
https://github.com/cv007/Avr01Dx_Twi

Here is the test project (this is the 3.33Mhz version, 20Mhz version just changes F_CPU and turns off the clock prescale)-
Code: [Select]
#include "MyAvr.h"
#include "twis.h"

// ./eepmake eeprom_settings.txt eeprom.eep
// xxd -i eeprom.eep
const u8 eeprom_eep[] = {
  0x52, 0x2d, 0x50, 0x69, 0x01, 0x00, 0x03, 0x00, 0x96, 0x00, 0x00, 0x00,
  0x01, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x07, 0xfd, 0xb7, 0x0f,
  0x7c, 0x4d, 0xba, 0xa7, 0x9c, 0x44, 0xc8, 0x36, 0xed, 0x5a, 0xbf, 0x62,
  0x00, 0x00, 0x00, 0x00, 0x17, 0x19, 0x41, 0x43, 0x4d, 0x45, 0x20, 0x54,
  0x65, 0x63, 0x68, 0x6e, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x20, 0x43, 0x6f,
  0x6d, 0x70, 0x61, 0x6e, 0x79, 0x41, 0x54, 0x74, 0x69, 0x6e, 0x79, 0x33,
  0x32, 0x31, 0x37, 0x20, 0x43, 0x75, 0x72, 0x69, 0x6f, 0x73, 0x69, 0x74,
  0x79, 0x20, 0x4e, 0x61, 0x6e, 0x6f, 0xca, 0x20, 0x02, 0x00, 0x01, 0x00,
  0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xed, 0x6e,
  0x04, 0x00, 0x02, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef,
  0xc0, 0x01, 0xc0, 0xde, 0x9a, 0x52
};

static volatile bool IDdone = false;

bool twisCallback(twis_irqstate_t state, u8 statusReg){
    //return true if everything ok, false if you want to stop the transaction
    //assume true
    bool ret = true;
    static u16 eeIdx;
    static u8 wrcount;

    switch( state ) {
        case TWIS_ADDRESSED:
            ret = twis_lastAddress() == 0x50; //for us?
            wrcount = 0;
            break;
        case TWIS_MREAD:
            if( eeIdx >= sizeof(eeprom_eep) ) eeIdx = 0;
            twis_write( eeprom_eep[eeIdx++] ); //respond
            if( eeIdx == sizeof(eeprom_eep) ) IDdone = true; //will assume is done
            break;
        case TWIS_MWRITE:
            if( ++wrcount > 2 ){ ret = false; break; } //we only accept 2 byte address
            if( wrcount == 1 ) eeIdx = twis_read()<<8; //high address
            else eeIdx |= twis_read(); //low address
            break;
        case TWIS_STOPPED:
        case TWIS_ERROR:
            ret = false;
            break;
        }
    return ret;
}


int main(void) {

    twis_defaultPins(); //SCL PB0, SDA PB1, Pi  28 ID_SC  27-ID_SD
    twis_init( 0x50, twisCallback);
    sei();
    while( ! IDdone ){}

    //blink led after ID has been sent (happens rather early/quickly in the pi boot process)
    while (1) {
        PORTA.DIRSET = 1<<3; PORTA.OUTTGL = 1<<3;
        _delay_ms(500);
    }
}

/*
$ cat /proc/device-tree/hat/product
ATtiny3217 Curiosity Nano
*/       

I intentionally ran at a 'slow' 3.33MHz, and the avr is clock stretching as it only has 33 clocks to stay within the no-clock stretching window (irq's are being used). This Pi B does not seem to be bothered by the clock stretching. The (total) clock stretch time is 30us at 3.33Mhz, and if I change to run at 20Mhz instead, there is no clock stretching.

You could also just poll instead of using irq's, and although you would be blocking I imagine the hat is going nowhere until the pi reads the id info.
« Last Edit: August 01, 2022, 08:18:42 pm by cv007 »
 
The following users thanked this post: HwAoRrDk

Online PCB.Wiz

  • Super Contributor
  • ***
  • Posts: 1735
  • Country: au
Re: Emulate an I2C EEPROM with an AVR?
« Reply #5 on: August 01, 2022, 10:46:49 pm »
... This Pi B does not seem to be bothered by the clock stretching. The (total) clock stretch time is 30us at 3.33Mhz, and if I change to run at 20Mhz instead, there is no clock stretching.

Does that mean the Pi B hardware does support clock stretching ?
I guess in HW that ignores stretch requests, some modest stretch will just make the clock pulse more runt, so some apparent tolerance could exist ?

Did your tests stretch it enough to confirm if HW support exists ?
 

Offline cv007

  • Frequent Contributor
  • **
  • Posts: 841
Re: Emulate an I2C EEPROM with an AVR?
« Reply #6 on: August 01, 2022, 11:31:53 pm »
Quote
Did your tests stretch it enough to confirm if HW support exists ?
I'm not a Pi expert, and I did not test extensively. I created a simple test which worked, and was mainly done to exercise my avr i2c code.

I would think if I can stretch it for 30us (the total clock low time before an avr ack), the pi 3 must be ok with it. The broadcom datasheet shows an i2c register for a clock stretch timeout/count, but do not know where the source code would be for this eeprom id thing so do not know what they are doing or when/where. It appears to be taking place before the kernel is loaded, but I do not really know. There is also a few write/reads to 0x51 after, so not sure what else they are looking for. Without a hat/eeprom, it does a write(ack'd)/read(ack'd) for about 30 tries, then does about 3 for 0x51.

They probably made the specification to allow for the possibility they may need to bit-bang the id pins someday (or maybe there is a pi model that already does that), and then do not want to deal with clock stretching. Just a guess. In any case, one probably has to take their word for it and assume no clock stretching will be available. Either use a faster mcu speed if using irq's, or use polling code.
 
The following users thanked this post: HwAoRrDk, PCB.Wiz

Online HwAoRrDkTopic starter

  • Super Contributor
  • ***
  • Posts: 1551
  • Country: gb
Re: Emulate an I2C EEPROM with an AVR?
« Reply #7 on: August 02, 2022, 11:23:02 am »
My ATtiny3217 Curiosity Nano 'hat'. That does nothing.

Very interesting and useful, thank you very much! :-+

I intentionally ran at a 'slow' 3.33MHz, and the avr is clock stretching as it only has 33 clocks to stay within the no-clock stretching window (irq's are being used). This Pi B does not seem to be bothered by the clock stretching. The (total) clock stretch time is 30us at 3.33Mhz, and if I change to run at 20Mhz instead, there is no clock stretching.

Fascinating. So if the Pi does tolerate clock stretching (or at least a little), then why do they specify the requirement against in the Hat design guidelines? ??? I wonder if this is something to do with backwards/forwards compatibility. Like, perhaps older Pi models don't support clock stretching, but newer do - or vice-versa (e.g. Pi 4 doesn't).
 

Online PCB.Wiz

  • Super Contributor
  • ***
  • Posts: 1735
  • Country: au
Re: Emulate an I2C EEPROM with an AVR?
« Reply #8 on: August 02, 2022, 08:46:44 pm »
Fascinating. So if the Pi does tolerate clock stretching (or at least a little), then why do they specify the requirement against in the Hat design guidelines? ??? I wonder if this is something to do with backwards/forwards compatibility. Like, perhaps older Pi models don't support clock stretching, but newer do - or vice-versa (e.g. Pi 4 doesn't).
It can also come down to testing and version control.
The dominant use is with EEPROMs so the extra tests and code coverage for clock stretching are well travelled ground,
It should be easy enough to test on your system ?
You can also lower the i2c clock speed, to gain timing tolerance.  Some scope captures could show where the Pi waits.
 

Offline mikeselectricstuff

  • Super Contributor
  • ***
  • Posts: 13874
  • Country: gb
    • Mike's Electric Stuff
Re: Emulate an I2C EEPROM with an AVR?
« Reply #9 on: August 02, 2022, 09:07:36 pm »
Bear in mind that real EEPROMs don't typically do clock stretching, and many hosts that talk to them will not support it, so for a fully compatible emulation, it needs to be fast enough to work without clock stretching at the maximum expected clock frequency.
Fortunately you have the repeated start-condition time to get the read data ready, so should be doable in principle, though there might be some limitations  of the I2C slave peripheral and/or interrupt latency that could get in the way.
Youtube channel:Taking wierd stuff apart. Very apart.
Mike's Electric Stuff: High voltage, vintage electronics etc.
Day Job: Mostly LEDs
 

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 6142
  • Country: es
Re: Emulate an I2C EEPROM with an AVR?
« Reply #10 on: August 06, 2022, 09:32:08 pm »
Clock stretching is pretty weird, that's the slave holding the clock low, and the master is supposed to detect this, wait until it's released and resume the clock.
Definitely not the most common thing in I2C devices, specially in eeproms.

Do you really need IRQ? You'll get an interrupt for every start,stop, byte complete...
That's a lot of interrupts, might be better to just run a state machine with polling.
« Last Edit: August 06, 2022, 09:34:38 pm by DavidAlfa »
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 

Online HwAoRrDkTopic starter

  • Super Contributor
  • ***
  • Posts: 1551
  • Country: gb
Re: Emulate an I2C EEPROM with an AVR?
« Reply #11 on: August 10, 2022, 04:35:00 pm »
I changed my mind about doing it with an AVR, as I didn't want to wait for an ATtiny 1-series dev board to arrive, so I grabbed an STM8 dev board I had instead (STM8S003 are about the same price and capability as an ATtiny).

With help from cv007's previously-posted example, and ST's app note AN3281, I managed to cobble together some code on the STM8 to do the same job. And... it works! :D My Raspberry Pi successfully thinks it has a Hat board connected, when in fact all it has is an I2C connection to an STM8 dev board instead.

Code: [Select]
pi@raspberrypi:~ $ ls -l /proc/device-tree/hat/
total 0
-r--r--r-- 1 root root  8 Aug 10 17:10 custom_0
-r--r--r-- 1 root root  4 Aug 10 17:10 name
-r--r--r-- 1 root root 26 Aug 10 17:10 product
-r--r--r-- 1 root root  7 Aug 10 17:10 product_id
-r--r--r-- 1 root root  7 Aug 10 17:10 product_ver
-r--r--r-- 1 root root 37 Aug 10 17:10 uuid
-r--r--r-- 1 root root 24 Aug 10 17:10 vendor
pi@raspberrypi:~ $ cat /proc/device-tree/hat/vendor
ACME Technology Company
pi@raspberrypi:~ $ cat /proc/device-tree/hat/product
ATtiny3217 Curiosity Nano
pi@raspberrypi:~ $ cat /proc/device-tree/hat/uuid
62bf5aed-36c8-449c-a7ba-4d7c0fb7fd07

I made a logic analyser capture of the whole boot-time I2C probing, and instrumented my MCU code with IO pin toggling to show which parts of my I2C ISR it was in. Bottom trace is the entire ISR; four traces above sub-parts of the ISR.



It seems to work pretty well, with ISR execution times quicker than I expected. I don't see any evidence of any significant clock stretching. Maaaybe just a little on the first byte of data read in every transfer of multiple bytes, but only by about 1us (SCL low period is about 6.25us versus nominal ~5us). And, that is probably caused by my extra code which attempts to fill a FIFO buffer with an activity log of the reads, to print over the UART on the main loop. If I got rid of that, the slight clock stretching would undoubtedly disappear.

Thank you again to all who helped. :-+
 

Online HwAoRrDkTopic starter

  • Super Contributor
  • ***
  • Posts: 1551
  • Country: gb
Re: Emulate an I2C EEPROM with an AVR?
« Reply #12 on: August 10, 2022, 04:37:18 pm »
By the way, I think I might know what the extra probing by the Raspberry Pi to I2C slave address 0x51 might be. It seems a bunch of common RTC chips use that address, so it might be probing for the existence of one of those.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf