Feed on

I’m thinking about updating the Standalone Temperature Logger to use an external EEPROM and since one of the ATtiny85 pins which correspond to hardware I2C/TWI is used by V-USB, a software implementation is the way to go. I found Soft I2C Master in Arduino code which allows any 2 pins to behave as an I2C interface.

Naturally I decided to convert that code to pure AVR which is available for download here – AVR_Soft_I2C_Master_v1.0

My modifications to the Soft I2C Master code are:

  • Made to work directly with AVR (De-Arduino existing code)
  • Changed from C++ to C
  • Added soft_i2c_eeprom_read_byte and soft_i2c_eeprom_write_byte

Example code

Here’s my example which writes 1 byte, reads that same byte and blinks the LED if the byte is the same.

#define ledPin (1<<PB2)
#define EEPROM_ADDR 0x50
sei(); // Turn on interrupts
DDRB |= ledPin;

byte address = 0;
byte writeByte = 170;
byte readByte = 0;


// Write 1 byte to the EEPROM
soft_i2c_eeprom_write_byte(EEPROM_ADDR, address, writeByte);

// Read 1 byte from the EEPROM
readByte = soft_i2c_eeprom_read_byte(EEPROM_ADDR, address);

// Blink LED if received byte matches written byte
if (readByte == writeByte) {
  PORTB |= ledPin;
  PORTB &= ~ledPin;


Verify it works

I won’t explain I2C as there already are lots of tutorials around but we can verify that this software I2C works correctly by looking at the SDA (blue) and SCL (yellow) lines when writing and reading. I’m using a 4Kbit (512 bytes) EEPROM which we only send the address as 1 byte, if you use say a 256Kbit(~32K bytes) then you  send the address as 2 bytes, this is so we can send an access request to an address more than 255.

We send a write to the device 0x50, send the address to write to 0x00 and the data to write 170 (0xAA). What isn’t show is that we wait 10ms before we perform any other operations on the EEPROM.

Now we read the data by sending a write to the device 0x50, send the address to read from 0x00, send a read to the device 0x50 and we receive the data which reads as 0xAA (170).

If you want to learn more about I2C EEPROMs I recommend taking a look at a datasheet, Atmel’s datasheet for the AT24C32 is a good starting point.

15 Responses to “AVR Soft I2C Master with example for I2C EEPROMs”

  1. Arti says:

    Your code seems to be great but does not work when using higher frequencies (12Mhz in my case). Anyone who want’s to use this code with faster atmega chips has to add some _delay_us() calls after seting clock hi to make it work. 🙂

  2. Mohammad says:

    Thanks for the codes .
    What modifications can I apply to use this code for memories with more than 1byte address?

    • Alex says:

      Hi Mohammad,

      You don’t need to do anything, as it’s already there in the code, if the read address is more than one byte (255), then it will send the high byte and then the low byte.

      // Send the address to read, 8 bit or 16 bit
      if (readAddress > 255) {
      if (!SoftI2cMasterWrite((readAddress >> 8))) return false; // MSB
      if (!SoftI2cMasterWrite((readAddress & 0xFF))) return false; // LSB
      else {
      if (!SoftI2cMasterWrite(readAddress)) return false; // 8 bit

      • Mohammad says:

        Dear Alex,
        Thanks for your help.
        The problem that I’ve faced is that I’ve modified this code to be suitable for AT90USB162+At24c512 or At24C08 eeprom in IAR systems.
        As I simulate this code in proteus I don’t get the signals above .
        I only see the “address of the device” part!
        The other question is that:Why have you assigned 0x50 for the device?!
        Is it possible!?(#define EEPROM_ADDR 0x50)
        I’ll be so grateful if you help!

        • Alex says:

          That’s odd, what if you manually pulse the data line high and low, does that work?

          The device is assigned 0x50 because I left the address pins floating which means read as 0, so in binary it would be 1010000 or 0x50 in hex (the last bit which isn’t included is the read/write bit, we left shift our eeprom address by 1 to the left to add the read/write bit on the end (LSB)).

          You need to check your EEPROM’s datasheet to see what address you receive if the address pins are high or low.

          • Mohammad says:

            Dear Alex,
            I really appreciate your help!
            Finally,I could read and write the eeprom(in proteus) .
            The problem was related to the address of the device . If you modify the device address to be standard as Atmel’s datasheet such as 0xA0,it will be very easy to understand .
            But,unfortunately I can’t read the contents of addresses from 0 to 255!

            It is important to note that I have changed the “address” declaration into “uint16_t” .
            But I read the contents of addresses more than 255 successfully .
            Do you have any idea about that?!

            • Mohammad says:

              Wow!I found the solution!!!
              I edited the read and write functions and made it suitable to send only 16bit addresses .
              Thanks Alex for everything!

              • Alex says:

                Good to know, my code that I wrote back then must have not accounted for writing 2 bytes even if the address was lower than 255.

  3. Mohammad says:

    Dear Alex,
    Hi!I really appreciate your former guides about the project.I’ve faced a problem writing in addresses more than 255!!!For all the addresses I enter,it reads only one data!I don’t know whether it writes in that specific address.May you please help?!
    I really appreciate it.

    • Alex says:

      Hi Mohammad,

      I thought that you had fixed the address issue?
      If the only issue you have now is the one byte of data, then with most memory types you can enable sequential mode. When you do that, you just write the address and then you can keep writing 0xFF (or anything) and it will return the next byte of data.

  4. Mohammad says:

    Hi Alex,
    The exact problem is that I like to write data in .e.g address 0x032C of the eeprom(my eeprom is AT24C08) .for addresses 0x0000 to 0x00FF,I have no problems with write/read operations,but for addresses 0x0100 to 0x03FF I can’t read valid data from eeprom.
    Are you sure that this code is working well for eeproms having addresses more than a byte?!
    How can I change the code to be suitable for my application?!
    I really appreciate your help.

  5. Aniket says:

    I am developing application for communicate between Two Atmega164P,
    I debugged The program in simulator, but After sending Start condition
    I didn’t get Expected Status from TSWR register.
    Here I attached Program which is developing into Atmel Studio 6.2



    .DEF uartChar = r1 ; Storage for character received from UART
    .DEF tempReg = r21 ; for temp purpose

    ; TSWR response for Acknowledgement from slave
    .equ I2C_SLA_W = 0×04
    .equ I2C_START = 0×08
    .equ MT_SLA_ACK = 0×18
    .equ MT_DATA_ACK = 0×28

    .org 0×0000
    ; irq table
    rjmp main ; Reset Handler
    .org 0×40
    ; Main Function For Program


    ldi tempReg, 0×73 ; op port for DMX > 01110011
    out DDRC, tempReg ; Set PORTC 0.4 AND 0.5 to 1 To enable DMX in Transmitter Mode
    out PORTC, tempReg ; 0.0 > SCL and 0.1 SDA

    ldi tempReg, 0xF0 ; op port for LED
    out DDRD, tempReg ; USART0 and USART1 in
    out PORTD, tempReg


    call i2cInit
    call i2cStart
    sbi PORTD, 4
    call i2cAddressSlave

    ldi tempReg, ‘J’
    mov uartChar, tempReg
    call i2CDataTransfer
    sbi PORTD, 5

    ldi tempReg, ‘A’
    mov uartChar, tempReg
    call i2CDataTransfer

    ldi tempReg, ‘S’
    mov uartChar, tempReg
    call i2CDataTransfer

    call i2cStop

    rjmp MainProgram

    ;*******************************Initialization of I2C*************************************

    ldi tempReg, 0×00
    sts TWSR, tempReg
    ldi tempReg, 0x0C
    sts TWBR, tempReg
    ldi tempReg, (1<<TWEN)
    sts TWCR, tempReg

    ldi tempReg, (1<<TWINT)|(1<<TWSTA)|(1<<TWEN) ; Send START condition
    ; -set TWINT/TWSTA and enable TWI
    sts TWCR, tempReg ; load to TWI control register
    ldi tempReg, (0<<TWSTA)|(1<<TWEN) ; Send START condition
    ; -set TWINT/TWSTA and enable TWI
    sts TWCR, tempReg ; load to TWI control register

    lds tempReg,TWCR ; Wait for TWINT Flag set. This indicates
    sbrc tempReg,TWINT ; that the START condition has been transmitted
    rjmp waitForStartSend ; if TWINT flag is set

    lds tempReg,TWSR ; Check value of TWI Status Register.
    andi tempReg, 0xF8 ; Mask prescaler bits.
    cpi tempReg, I2C_START ; If status different from START (0×08) go to ERROR
    brne ERROR ; Set LED on PORTD0.5 as error signal and terminate I2C

    i2cAddressSlave: ; Entered into Master transmitter mode
    ldi tempReg, I2C_SLA_W ; Load SLAVE ADDR = SLA_W (0×04) into TWDR Register.
    sts TWDR, tempReg ; Send Address
    ldi tempReg, (1<<TWINT)|(1<<TWEN) ; Clear TWINT bit in TWCR to start transmission of
    ; address
    sts TWCR, tempReg ; load TWCR

    lds tempReg,TWCR ; and ACK/NACK has been received.
    sbrc tempReg,TWINT ; Wait for TWINT Flag set.
    rjmp waitForSlaveAddrSend

    lds tempReg,TWSR ; Check value of TWI Status Register.
    andi tempReg, 0xF8 ; Mask prescaler bits.
    cpi tempReg, MT_SLA_ACK ; If status different from MT_SLA_ACK = 0×18 go to ERROR
    brne ERROR

    mov tempReg,uartChar ; Load data byte
    sts TWDR, tempReg ; Load DATA into TWDR Register.
    ldi tempReg, (1<<TWINT)|(1<<TWEN) ; Clear TWINT bit in TWCR to start transmission of
    ; data
    sts TWCR, tempReg

    lds tempReg, TWCR ; This indicates that the DATA has been transmitted,
    sbrc tempReg, TWINT ; and ACK/NACK has been received.
    rjmp waitForDataSend

    lds tempReg,TWSR ; Check value of TWI Status Register.
    andi tempReg, 0xF8 ; Mask prescaler bits.
    cpi tempReg, MT_DATA_ACK ; If status different from MT_DATA_ACK =
    ; 0×28 go to ERROR
    brne ERROR

    ldi tempReg, (1<<TWINT)|(1<<TWEN)|(1<<TWSTO) ;Transmit STOP condition
    sts TWCR, tempReg
    call i2cStop
    clr tempReg
    out PORTD, tempReg
    rjmp MainProgramEnd

Leave a Reply to Mohammad