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.

No comments: