23 February 2013

Tutorial 20c: The 24xx08

While setting up the USI for I2C is more complicated, the value of that complexity becomes apparent when you look at a real implementation of the I2C protocol. The Microchip 24AA08 EEPROM is the I2C equivalent to the 25AA080 we used as an example for SPI. The I2C version of this EEPROM is substantially simpler.


This chip, unlike its SPI counterpart, has no status register. Write protection is handled strictly by hardware through a single pin connected to either ground (write enable) or Vcc (write protect). There are really only two instructions, and the read and write instructions can be done by single bytes or by up to 16 bytes (a page). The seek instruction is really nothing more than a write instruction without sending any data to write.

  • Seek: The 24xx08 has a register that points to the current address in its memory for the next read/write command. We can change the current address by sending the slave address + write bit (a 0 in the 8th bit sent) followed by the address to which the slave should point. Each of these is, of course, followed by an acknowledge bit. The master issues a stop condition immediately after the address.
  • Write: We write a value to the 24xx08 by sending the slave address + write bit followed by the memory address to which the value should be written. This is followed by the 8-bit value to write and a stop condition. We can write up to 16 bytes in contiguous memory space by sending subsequent values before the stop condition.
  • Read: We read a value from the 24xx08 by sending the slave address + read bit (a 1 in the 8th bit sent). After acknowledging the command, the slave sends the 8 bit value at the current address on the next 8 clock cycles and increments the address pointer to the next space. If the master acknowledges, the slave will repeat by sending the next value. When all values required are read, the master sends a nack, indicating no more data is to be sent, and then issues a stop condition. If a value is required from a different address than the one to which the 24xx08's register currently points, a seek instruction should be sent before the read instruction.

Design Considerations

A quick look at the datasheet for the 24xx08 is worthwhile. There are a few things to note about this device.
  • Address: The 7-bit binary address for this device is 0b1010XBB. The bit labeled X is unused in this device, and can take either value. The last two bits BB refer to the block within the chip. The memory space is divided into four, 256-byte blocks. (Each byte within a block has an 8-bit address between 0 and 255. The X bit here would be used in a larger device with 8 blocks in memory.) Note that this means that only one of this particular device can be used on a given I2C bus; adding a second 24xx08 device, or any Microchip I2C EEPROM in fact, would create an address conflict on the bus. Adding the 8th R/W bit, this corresponds to slave addresses from 0xA0 to 0xA7 in hex. (That uses 0 as the X bit. Technically a 1 can be used here as well, so the addresses 0xA8 through 0xAF are respectively equivalent in this device.)
  • Most packages of this chip have three pins labeled A0, A1, and A2. In this particular device, these pins are unused and unconnected internally. Larger devices would use these in place of the block bits to determine the chip's address, allowing for up to 8 of the same chip on the bus. For this chip, the pins can be grounded, tied to Vcc, or even left floating.
  • Most of the timing requirements specified in the datasheet are good to see, but not an issue at the transmission rates we'll be using. (The device is rated for up to 400 kHz, and even at that rate the minimum requirements are well below what is actually used.) The important one for our purposes, however, is the maximum rise and fall times, TR and TF. These define the maximum RC time constant for the SDA and SCL lines. This device is rated with a 10 pF capacitance on the lines. The GPIO pins of the MSP430 are rated at 5 pF. The lines connecting the two chips will add a little more capacitance. Using a worst-case estimate of 30 pF and the maximum rise/fall times of 300 ns, we get a maximum resistance value of 10 kΩ.
  • Faster rates, as you recall, will benefit from lower resistances, at the expense of power consumption. The internal resistors available in the MSP430 are between 20 and 50 kΩ. For slow data rates, these are probably alright to use. But faster rates may require an external resistor in the 1 to 10 kΩ range.
  • Like the SPI version of this chip, the write process is not initiated until a stop condition is seen, and a 5 ms settling period is required for write operations whether writing a single byte or a full 16-byte page. Using a similar setup to the previous tutorial, we'll have to keep that in mind when using a 9600 baud UART to send the data being written. If any instruction is issued to the device during this settling time, the chip will not respond. The master can identify the nack by polling, in this case, and wait until an ack bit is sent before sending the next instruction.
And that's really about all you need to know to use this EEPROM successfully with the MSP430! The wire connections are just as straight forward, as only 4 wires are needed: Vcc, ground, SDA, and SCL. The WP pin of the 24xx08 should be tied to ground for write operations.


This example starts off simply, writing a single value to every memory space one by one. Since we need 5 ms between write commands, a simple delay is used in the code. Ideally, you would allow the MSP430 to continue with other tasks, and have a timer to indicate when it is ready to write again, but for this example a simple method works fine. The code provided uses a default fill value of 0xff, to act as an eraser of the flash memory. Feel free to change the fill_char value to whatever you wish; hex, decimal, and C character values are all acceptable here.
This example reads the entire contents of the EEPROM and dumps the data via UART. If you ran the i2cerase_G2231.c code previously, you should see 1024 instances of 0xff appear in your terminal (assuming it has the capability of displaying this code; you might change to a visible character if not). In this example, I've used a low power mode to hold the main function until a value has been read by I2C  Uncommenting the _low_power_mode_off_on_exit() command at the end of USI_ISR signals the data is ready to be transmitted by UART. A delay loop is used again to delay the next read, though ideally a low power mode could be used again here.
The final example demonstrates writing in full pages of 16 bytes. The first 1024 characters received by UART are written to the EEPROM. Like the SPI example, you can use a text file to send particular ASCII data to the EEPROM. After sending the data, use i2cread_G2231.c again to display the message on your terminal. The page write itself is used to accommodate the EEPROM settling time: the amount of time it takes to send 16 bytes at 9600 baud is longer than the required 5 ms.

Race Conditions

When designing these examples, I ran into a bug that took me a long while to solve, so I thought I'd mention it here. What I finally figured out was that I was hitting what's called a race condition. Originally, my main function in i2cwrite_G2231.c looked like this:
    for(i=0; i<64; i++) {
     slave_address = SAdrs + i/16; // Current Block Address
     pageBuffer[0] = (i%16)*16; // EEPROM Write Address
     for(j=1; j<17; j++) {
     _BIS_SR(LPM0_bits + GIE);   // Standby for UART RX
    pageBuffer[j] = RXBuffer;   // Extract RXBuffer
    while(SCFG & BIT4) // Ensure previous transmission is complete
     SCFG |= BIT3; // Mark transmission/write instruction
    USICTL1 |= USIIFG; // Set USI Interrupt to start
    // I2C communication
The problem was that the first element of pageBuffer would be overwritten shortly after the USI interrupt was started-- before the USI could transmit the original value! Once you start mixing peripherals and making extensive use of interrupts, you need to be very careful of race conditions: when values you want to use are changed before you actually use them. The solution here, as seen in the linked code example, was to not change the slave and memory addresses until a new page of 16 bytes was ready to be sent. That prevented the race condition by ensuring the value was not changed until the USI was clear.

Note that there is still a potential race condition due to the UART: if you speed up the UART (likely you'd need to slow down the I2C bus too), you could receive new data before the old data is transmitted to the EEPROM. Of course, increasing the UART speed could cause problems with the settling time as well.

That concludes this particular tutorial; I'll move on to some other topics now, and bring a new experiment to the blog that will illustrate a fun use of I2C.

06 February 2013

Tutorial 20b: USI & I2C

Implementing I2C on the MSP430 USI is more involved than SPI. One of the major problems is that part of the implementation must be done in software; the USCI module is a more complete hardware implementation that makes I2C communication easier, but not all devices have that module. For those devices that don't have a serial module at all, I would recommend using SPI instead. It's possible to do I2C completely in software and timers, but it's not easy. SPI is much simpler for "bit-banging".

When you consider how a device should react to an interrupt, there are three possibilities. First, there may be only one task to be done. This reaction is the simplest, and is straightforward to code. A good example is using the timer interrupt to toggle the state of an LED. Second, there may be a set of possible tasks, and a given set of circumstances determines which task is performed at the interrupt. Often this is accomplished through use of if/else statements. Finally, there may be a sequence of tasks to be performed, each step timed by a succession of interrupts. This final reaction is also described as a "state machine".

The USI module has, of course, only one interrupt. (Well, there are two, technically, if your device is configured as a slave: there is a separate interrupt for signalling the start condition for an I2C transaction.) This interrupt is triggered every time a non-zero value is written to USICNT.  Every I2C transaction will require more than one write to USICNT, and we can make use of that to set up the module as a state machine. Doing so makes it easier to understand what is happening and to debug problems, and also allows you to spend more time in a low power mode. Let's examine a flow chart describing the state machine for an I2C master:

I've tried to group these in a suggestive way, separating the different states as they will appear in code. Note that in particular the logic analyzing the Ack/Nack bit will be in the same section of code that handles the transmit/receive of the data. I've also left out the Ack/Nack handling for dealing with multiple bytes of data to keep from cluttering the diagram, but be aware that those will also be handled.

Note that the state machine diagram for a slave configuration is not much different--replace the first two steps with a receive address and acknowledge if needed. (A "Nack" response would simply be to go right back to idle in this case.) Instead of generating the stop condition, a slave would simply clean up (perhaps taking a new measurement or otherwise preparing for the next command) and go back to idle.

The state machine concept lends itself very easily to the switch statement in C. Looking at this state diagram, we should be able to code the USI interrupt with 6 different states. The states can be assigned a number (eg. 1-5 + default for idle), or by using the enum construction in C, making the code much more readable.

Configuring the USI for I2C

The USICTL0 register is handled similarly to the setup we did for SPI. The main difference is that we no longer need P1.5, since there is only one data line in I2C. In USICTL1, we also need only enable the USII2C bit. For I2C operation, as you recall, we want the phase/polarity to match Mode 3 in SPI; USICKCTL should be set to whatever clock source and division are desired, with the USICKPL bit enabled.

Finally, when operating the USI in I2C mode you should disable automatic clearing of the interrupt flag by enabling the USIIFGCC bit in USICNT. The reason for this will become apparent when we look at the interrupt service routine. Since one of the upper bits in USICNT is now enabled, we can't start transmission/reception simply by assigning the number of bits to this register. For that reason, we'll start the clock by using an or operator to avoid changing any of the upper bits in this configuration.

The code to setup the USI for I2C may look something like this:

// Enable ports and set as master
USICTL1 = USII2C + USIIE;       // I2C mode, enable interrupts
// Use SMCLK/8, Mode 3 phase/polarity
USICNT |= USIIFGCC;             // Disable automatic clear control
USICTL0 &= ~USISWRST;           // Enable USI
USICTL1 &= ~USIIFG;             // Clear any early flags

Setting Up the ISR

This (updated and verified!) code fragment is a simple way to set up the ISR in a way that allows both transmission and reception with I2C. There are a few things to note about this code:

  • The format for usage of the enum C type is:
    enum{item_1, item_2, ... , item_n} var_name = initial_state
    The numbering as done in this example is redundant, but serves to illustrate that explicit enumeration values can be given. This variable is declared as static to retain its value between calls to the ISR.
  • Recall that the start and stop conditions are given by changes in SDA while the clock is high. This is handled by enabling the output latch for the SDA line using the USIGE bit in USICTL0. The SDA line will immediately take whatever value is in the first bit to send in USISRL, and so that register is set to 0x00 for start and 0xFF for stop. (Only the most significant bit need be set, but this method has the benefit of being both convenient and comforting.)
  • Also recall that in I2C, the SDA line can be changed by master or slave. When receiving, control of the line must be relinquished to whomever is transmitting. This is accomplished by setting the USIOE bit in USICTL0 to control the line, and clearing it to relinquish the line.
  • The SCFG flag I have used in the prior tutorials on serial communication adds two more bits used here: bit 3 indicates transmit when set, receive when clear. Bit 4 is used to indicate a new receive value has been saved to the buffer. Presumably the buffers ITXBuffer and IRXBuffer have been defined previously. (The names here are thinking ahead to using I2C in parallel with UART.)
  • I have also used a hopefully self-explanatory variable slave_address, presumably defined earlier.
  • Note the inclusion of an additional state labeled "PrepStop". This state is necessary before sending a stop condition, as the line must be low before a stop can be sent. Note that each case in the switch statement ends with setting one or more of the first 5 bits in USICNT and setting the next state. After the break, the USIIFG is cleared. When the USI module has finished transmitting everything, an interrupt is generated, and the routine will call into the next state as a result. The PrepStop state is needed in order to allow the previous ack/nack to finish before stopping the communication.
  • Finally, clearing USIIFG is handled manually in order to stretch the clock when necessary for a slow communication. If something delays the master, clearing it manually ensures that every step in the ISR is followed before another interrupt can be allowed.

That finishes this tutorial on the configuration of the USI module for the I2C protocol. Next time we'll put it to use with the 24xx08 serial EEPROM, as we did for the SPI example.