Sunday, July 10, 2016

Many EEPROMs inside of one / inject additional bits into I2C bus

The goal

Emulate few smaller eeproms with one bigger.


Extend any device using i2c eeprom with memory banks!


  • 1 x Attiny45
  • 2 x 4.7k Ohm
  • 1 x 4.7k / 1k Ohm (optional resistor)
  • 1 x at24c512 eeprom
  • 1 x micro switch



Many devices uses eeprom as permanent memory for small bunch of settings, e.g. TVs use it to remember last used channel after you disconnect the power cord, stereo sets remember last FM station used etc.

So what if we want to have many banks for TV/FM radio or any other device? Usual way I have seen on the internet, was to add additional eeprom into i2c bus, and change state of A0,A1,A2 pins which are used to determine overall eeprom address. This solution works quite well, but if you need more banks, you just need to install more chips, power consumption will slightly increase, but more space need to be used and in my case this was a signal to think about find another way.

How looks the address space of typical eeprom?

When your device has e.g. 24c64 i2c eeprom onboard, it meas that it has 8192 bytes that can address. During i2c transmission, two bytes are used to address memory cells, so address space for 8192 bytes would look like:


You can see that 3 most significant bits of first memory address byte is unused. Now have a look on address space for 24c512 i2c eeprom chip, you can store 65535 bytes of data in it.


Every bit in first byte of address space is used.

So what if we swap 24c64 with 24c512, will it work? - Yes it will, but your device will address it only to max 00011111 11111111 cell, because firmware doesn't know that you swapped the chip with bigger one.

If we inject additional bits into i2c bus during normal operation, we can have up to 8 separate 24c64 memory cell address spaces in one 24c512, in short it means that you can have 8 memory banks in one 24c512, no need to solder additional chips. Yay!

After injecting additional bits into i2c bus, the address space for "memory banks" looks like below:

bit address
bit address
000000000 0000000000011111 1111111108191
100100000 0000000000111111 11111111819216383
201000000 0000000001011111 111111111638424575
301100000 0000000001111111 111111112457632767
410000000 0000000010011111 111111113276840959
510100000 0000000010111111 111111114096049151
611000000 0000000011011111 111111114915257343
711100000 0000000011111111 111111115734465535

How and when inject additional bits?

We should inject them when the first address byte is clocked to the memory. To set those bits up, we need to pullup SDA line for a moment, then remove pullup from line, SDA now should be aka in high impedance seen from our side, so the I2C master device could clock the rest of the bits.

Sometimes you need to add additional resistor between device and eeprom, and connect our "injector" directly to SDA and SCL eeprom lines, this will prevent short seeing from our "injector", because mcu from master device probably pulls down SDA when clocking this unused 3 bits (unused from 0 - 8191 address space perspective).

First address byte will appear after start condition is seen following write command. This is the place when we do inject, but only for write command, reads must be filtered out, otherwise we inject bits into first read eeprom byte.

To ignore reads, we check which eeprom address was used, in my case 0xa0 is for write, 0xa1 for read, so only need to sense last bit in this command.

Eeprom command "read current cell" - 0xa1

The i2C bus "bit injector"

Should be small, to save as much space as it can (without additional oscillator), and be fast to handle i2c clock. One device that meet this criteria is Attiny25/45/85, smd version has SO8 housing, so it's only slightly bigger than 24cXXX eeprom and it could be clocked from internal PLL up to 16Mhz system clock. I mean WOW, didn't know before that Attiny's has PLL's, I was using atmega8/32 in my projects.

I2C Attiny45 bit injector

Attiny25/45/85 has one external interrupt INT0 which could be triggered by faling/rising edge and high/low state, and PCINT0 which is pin change interrupt. This set is sufficient, since we need only to find start condition and then check SDA state when SCL is clocked.

Start condition occurs when SDA state gets hi-low and SCL is high, so setting INT0 to falling edge and simple check of SCL pin is all that we need.
SIGNAL(INT0_vect) {  // SDA
   // check if start condition is met
   if(BS(PINB, PB1)) {
      start = 1;
      clk = 0;
      // make sure the WP is low at this time
      L(PORTB, PB0);
Below you can find i2c bus state during start condition.

I2C start condition

SCL attached to PCINT0, gives us two interrupts per i2c clock tick (on low-hi and hi-low transition). This is very good deal for higher transmission speeds because if want to check memory address clocked to device, we look on low-hi change. Checking SDA state eats some time, so when we start to check it on low-hi SCL change, for sure we hit stable SDA state.

Stable I2C SDA pin state timing

When we start to inject additional bits, we look only for hi-low change, enabling/disabling pullup on hi-low transition assures that SDA state change will occur in the SCL low state, hence again we trigger stable SDA state when SCL is high.

That's why in source code you can see, that we're counting up to 15 to sense read command (last bit from 0xa1).
SIGNAL(PCINT0_vect) {  // SCL
   // start == 1 means that we have seen start condition
   // now, start processing
   if (start) {
      switch(clk) {
         case 15:
            // if this is a read command - ignore the rest
            if (BS(PINB, PB2)) {
               start = 0;
Below you can find i2c bus state when issuing 0xa1, "clk" variable values are on the picture.

15th PCINT0 is time for '1' check to sense read command

How bit injection look from bus perspective?

It looks like the master i2c device would clock them into eeprom. In example below i2c master is always clocking '0' value as the fist byte address, but gets different data on each bank.

Banks are changed by pulling PB4 from PORTB, down.

Sending first address byte (0) for bank 0 (no inject)

Sending first address byte (0) for bank 1

Sending first address byte (0) for bank 2

Sending first address byte (0) for bank3

Bonus - extend life of your eeprom!

Let's get back to TV example, when you jump from channel to channel, because nothing interesting is at your cable. Probably after some years of this behavior your TV's eeprom will be dead, there are two reasons for this. First is that eeprom memory has limited write cycles, second is manufactures eventually like to sell you newer model, so they design mcu firmware in a way that writes are used more often. Most common case is that your older TV in some point of time started to forget channel settings, after eeprom swap everything went back to normal.

How to prevent this? My i2c injector simply check write address if equals "last channel number cell" (that I figured out with logic analyzer), it simply puts high state on WP pin of 24cXXX. High WP state will enable Write Protect mode, and none of eeprom cells will be written. This could extend life of your eeprom. Of course your TV won't remember last channel used.

Blocking write to specified address

Want some really useful example?

Baofeng UV-82 is a cheap handheld dualband radio, it covers VHF 136-174MHz and UHF 400-520MHz. You can program 128 channels into its memory bank via CHIRP software, has also scanning features etc.

Scanning in this model is defined during programming from CHIRP, so you can mark channel as 'S' (skip) and radio will ignore it during scan and jump between those without skip flag.

Ok, so you want to unflag specified channel or flag other during normal radio operation - nope that's impossible, you need to connect it to CHIRP and program again ... 

Also I found on the net that some people using this radio for ham which travel across countries would like to change programmed channels in the fly. In different countries ham channels can have different frequencies etc..

Now it's possible!

Below you have PoC example with at24c256 which gives 4 banks for this radio, the best thing is that you can program each bank via chirp, so it's transparent for the MCU on the radio and for the programming software. There is one exception from this rule, but I'll write it more in sperate post about adding banks to Baofeng UV-82.

Baofeng UV-82 programmed via CHIRP and Bus Pirate

Additional bonus is that ALL settings could be different on each bank, e.g. power level, backlight, not only channel names and frequencies. You can have hidden settings thanks to this.

In example below, I programmed 4 banks (bank 0,1,2,3) with PMR frequencies via CHIRP and BusPirate, one difference across them is message displayed when you power on the radio, in my case this is BANK 0,1,2,3.

Programming Power-On Message via CHIRP

Currently I'm waiting for at24c256 and at24c512 smd version shipping, when I finish the radio modification and put it back in it's case, I'll make detailed post describing whole procedure.

I also tracked down RESET pin of UV-82's MCU unit, and after changing bank number RESET is triggered to reread eeprom contents from different address space.

Baofeng UV-82 with 4 memory banks on single eeprom and attiny45 i2c bus bit injector

Cool, right?

Here are some photos of finished version. I forgot to flip Attiny45, so I needed to add some additional connections. If I would flip it, the SDA and SCL lines would match perfectly Attiny's pins, so the overall number of wires would decrease.

Attiny45 on the top of at24c512 eeprom

EEprom + Attiny on takes up a little place

Glued together


  1. Yeah. Cool! Would be nice option to add extra channels to a BF-888s ;-)

    1. If you program it via CHIRP, use a hex editor to preview the .img file that you put on the radio. In my case it looked like whole eeprom content. Maybe in yours is the same. Size of my file was about 6k, so less than 8192 (24c64 size). Or grab screwdriver and see on own eyes ;)