Emulate an I2C EEPROM with an AVR? - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: July 31, 2022, 07:05:19 pm
content: 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 theRaspberry Pi Hat design guidelines:QuoteThe 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 itmightbe possible to handle the EEPROM reads without clock stretching, as there is an Atmel app noteAVR290giving an example implementation of an I2C slave giving "no clock stretching – true 100kHz operation".Has anybody done this before? Or have any thoughts?

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 01, 2022, 02:05:55 pm
content: Just setup the AVR as slave I2C.Make state machine, checking for start/stop conditions, the protocol is very simple.

author: hans
author_rank: Super Contributor
timestamp: August 01, 2022, 02:28:17 pm
content: 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.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: August 01, 2022, 05:09:08 pm
content: 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. Thatseemslike 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?Quote from: hans on August 01, 2022, 02:28:17 pmYes, 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.Quote from: hans on August 01, 2022, 02:28:17 pmHmm, 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.

author: cv007
author_rank: Frequent Contributor
timestamp: August 01, 2022, 07:51:20 pm
content: 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_TwiHere is the test project (this is the 3.33Mhz version, 20Mhz version just changes F_CPU and turns off the clock prescale)-Code:#include "MyAvr.h"#include "twis.h"// ./eepmake eeprom_settings.txt eeprom.eep// xxd -i eeprom.eepconst 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 truebool 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++] ); //respondif( eeIdx == sizeof(eeprom_eep) ) IDdone = true; //will assume is donebreak;case TWIS_MWRITE:if( ++wrcount > 2 ){ ret = false; break; } //we only accept 2 byte addressif( wrcount == 1 ) eeIdx = twis_read()<<8; //high addresselse eeIdx |= twis_read(); //low addressbreak;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_SDtwis_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/productATtiny3217 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.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: August 01, 2022, 10:46:49 pm
content: Quote from: cv007 on August 01, 2022, 07:51:20 pmDoes that mean the Pi B hardwaredoessupport 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 ?

author: cv007
author_rank: Frequent Contributor
timestamp: August 01, 2022, 11:31:53 pm
content: QuoteI'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.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: August 02, 2022, 11:23:02 am
content: Quote from: cv007 on August 01, 2022, 07:51:20 pmVery interesting and useful, thank you very much!Quote from: cv007 on August 01, 2022, 07:51:20 pmFascinating. 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).

author: PCB.Wiz
author_rank: Super Contributor
timestamp: August 02, 2022, 08:46:44 pm
content: Quote from: HwAoRrDk on August 02, 2022, 11:23:02 amIt 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.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: August 02, 2022, 09:07:36 pm
content: 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 06, 2022, 09:32:08 pm
content: 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.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: August 10, 2022, 04:35:00 pm
content: 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!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: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 vendorpi@raspberrypi:~ $ cat /proc/device-tree/hat/vendorACME Technology Companypi@raspberrypi:~ $ cat /proc/device-tree/hat/productATtiny3217 Curiosity Nanopi@raspberrypi:~ $ cat /proc/device-tree/hat/uuid62bf5aed-36c8-449c-a7ba-4d7c0fb7fd07I 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.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: August 10, 2022, 04:37:18 pm
content: 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.