Inter IC (I2C) bus design and test for embedded systems: Part 1“No man is an island, entire of itself…,” John Donne
Likewise in embedded systems, a simple function rarely exists in isolation. A simple pushbutton and LEDs controller is useful, but if that controller cannot report when a user has pressed a button or share the status its LEDs indicate, the controller’s worth is lessened.
The key to increased value is communication and increasingly the Inter-IC (I2C) protocol, a 2-wire master/slave communications bus standard, is the communication bus of choice for embedded systems of all sizes.
I2C enabled devices are available as off-the-shelf, drop-in functions, but the true power of the bus is revealed when a custom device is created using a microcontroller. Customization enhances the fit; communication multiplies the value of the solution.
Before you proceed to incorporate I2C communications in your system, you face two strategic questions:
1) How to use I2C
communications in your system, and
2) How to verify your system is working properly?
In Part 1 I will answer the first question, concentrating on the design rules, things to consider for the I2C master, and guideline to a successful slave implementation. In Part 2, I will concentrate on test and verification, and you will see that design and test go hand in hand in any successful embedded project. Together we will describe a strategy for designing an embedded system (or subsystem) that uses I2C communication to increase its value, flexibility and reliability.
Introduction to I2C
I2C is a low- to medium-data-rate master/slave communication bus. Philips, the creator of I2C, describes the bus as a simple bi-directional 2-wire bus for efficient inter-IC control . The bus consists of just two wires or circuit traces, one for clock and the other for data, with a pull-up resistor on each wire of the bus. The physical layer is a simple handshaking protocol that relies upon open collector outputs on the bus devices and the device driving or releasing the bus lines.
The master supplies the clock; it initiates and terminates transactions and the intended slave (based upon the address provided by the master) acknowledges the master by driving or releasing the bus. The slave cannot terminate the transaction but can indicate a desire to by a “NAK” or not-acknowledge. Addressing opens the lines of communication between the master and its intended slave device and the master keeps the connection open until it wishes to terminate the connection (when the master is finished with the slave).
The simple hardware design and relatively low data rates allow any engineer to take advantage of I2C as a communication solution. You can learn more about the protocol by reading through the Philips specification and various application notes  (almost every I2C slave device also describes the specification with sufficient detail to gain a working understanding of it ).
Nearly every microcontroller vendor has taken care of the physical implementation of the bus, so you do not need to understand all the details in order to harness the power of I2C (just as you do not need to understand RS232 communications to be able to use devices with HyperTerm).
Off-the-shelf components like serial EEPROM, remote temperature sensors and I/O port expanders using I2C are available as slave devices. Since most microcontroller vendors offer I2C master and slave capabilities on most of their devices, there is nothing to prevent an embedded designer from taking advantage of I2C.
Creating a custom I2C slave device is the point of this article, and
it is easier than you think, as long as you keep to a plan and follow
some simple rules.
Simple Rules Lead to Success
Creating a custom I2C slave requires firmware: the application-general protocol (how data is interpreted from the physical layer/hardware registers), the application-specific interfaces (what data is exchanged, and functional implementation (what a specific piece of data means, what action does it cause). I have four rules to a successful I2C firmware implementation:
1) Make the master generic;
let the slaves specialize.
2) Use a standardized register-based protocol.
3) Use well-defined (unchanging) interfaces.
4) Follow an established set of design practices.
These rules are not hard to understand, nor are they hard to follow. Some are just reminders of what many embedded designers would do anyway. Without rules many small subsystems develop organically, and before you know it you're getting ready to ship something that you wish you could re-design (which the schedule seldom permits). Starting your design with these rules in mind will reduce your regret over the results.
#1: Make the master generic
and let the slaves specialize.
The master in an I2C design is often the master of the entire system, and as such has many things on its mind. Designing specialized slaves, that provide specific functionality shields the complex master in your system from the winds of change as the solution solidifies. Adding features to the slaves (as they inevitably creep in) will have little impact on the systems as a whole, when the master’s role is a generic one.
#2: Use a standardized
When you read the datasheet for an off-the-shelf I2C slave, you will find a description of a set of registers, what data they contain or what actions writing data to them causes. This set of registers looks to a master like a memory map.
The details of the registers may differ from slave to slave, but all follow a standard pattern: write the first data byte to set the internal register-pointer, and then write the second data byte to transfer the data to the register pointed-to by the register-pointer (the first data byte).
When a master wants to read from the slave, it first writes the register-pointer to the slave, then starts a read transaction and reads out the data pointed-to by the register-pointer. Many devices allow multi-byte reads and writes, but it is in this way that they differ from device-to-device.
If you use this same register-based protocol in your design, you benefit by having everyone intuitively understand the protocol. This makes a team more productive solving real problems (rather than debugging a new protocol). The standardization yields greater dividends because it enables a design (both the master and slave) to be more easily reused.
#3: Use well-defined
This is a tenet of any project, but it bears reiteration. Define the interfaces and honor them as contracts. In rule #1 I stated the master would be generic, the slave would carry the burden of the task. To be able to pull this off, the interface between the master and slave must be set early and set in concrete. Use simple, easy to understand interfaces. Keep the interfaces to the essentials, let the slave boil the data down or expound a command. The interface need not reflect the slave implementation details, in fact it is best not to.
#4: Follow an established set
of design practices.
This rule is less well defined, but the main point is that designs should be supported with established, accepted design practices. The practices will vary from team to team, but the point of this rule is that you want the team’s concentration to focus on the value-added function to be accomplished, and not worry about all the methods.
For an I2C slave there are two practices of particular usefulness: first, maintain separate foreground and background threads or tasks; second, partition the data interface into read-only and read/write blocks, and protect against writing to read-only registers.
Considerations for the I2C Master
It is the master in the system that often has the least flexibility. So many other requirements and concerns find a home in the master, that there cannot be many rules or guidelines imposed upon the master by a slave device. And yet, that is precisely what happens when off-the-shelf I2C slave devices are chosen.
Most require the master not only to configure the slave, but also often to repeatedly poll and command it. A custom I2C slave implementation using a microcontroller can shift the balance of the work into the slave, make life easier for the master, and therefore make it easier to add higher-value functionality to the system.
There are a few things to keep in mind with the master for a more successful implementation: plan for tolerance to a slave’s timing, use high-level commands rather than micromanaging the slave, and implement interfaces early then resist changing them.
While the master is in control of the timing at the physical layer, often a slave has many things to do at the functional layer. When a slave is designed to act upon a specific command, it may not be instantaneous, and therefore the master should not expect immediacy. A tolerant timing profile, where the master does not make snap decisions about a slave’s response, is the best approach.
Remember masters: be tolerant, and do not make snap decisions (if instantaneous action is required, we’ll see in the next section where the slave can help out).
Rule #3 above says to define interfaces and then follow them, but what these interfaces should be or should look like isn’t addressed. For the master’s benefit, create an interface of high-level commands; limit the interactions of the master. With a custom slave, the master need not even configure the slave (this can be programmed into the start-up section of the firmware), and it only needs to be contacted when an unusual event occurs (i.e. fault condition).
Lastly, since the main implementation task for the master is the interfaces, the earlier these interfaces are implemented and tested, the sooner the master can move on to its other demands. If the interfaces are defined, followed and consist of high level commands, the implementation burden on the master is light, and the actual requirements of each command can be explored in the context of the greater system.
Remember, the master will drive functional requirements for the slaves, so allow as much time as possible for the interfaces and the intents of the high-level commands to be hardened in the context of the system.
Considerations for the I2C Slave
When a custom slave is the goal, implementation of the slave is likely to be the largest task, and the fundamental reason of pursuing a custom slave device is to get a custom fit.
As discussed earlier, this custom fit can entail offloading the master of lower-level functions, or it can be as simple as multiplying the number of I/O pins for a host processor (without the burdens that off-the-shelf devices can impose). A custom device can serve as a buffer between several off-the-shelf I2C slave devices, again reducing the burden on the master or providing address arbitration for slaves that have none.
For whatever reason (there are so many) that you pursue a custom I2C slave, keep in mind the following guidelines to increase the value of your results: separate the firmware that handles the communications protocol from everything else, maintain coherent data at all times, and provide an “emergency signal” to the master to limit the master’s need to poll the slave.
The “perceived” main task of the slave is not communicating or managing the communications protocol. The truth is, if not handled correctly, or if designed incorrectly, the main design and debug task will be the communications.
The first step on the path to successfully managing communications was to adopt a standard register-based protocol, rule #2 above. The second step is to use a tested, debugged firmware implementation of that protocol. It may be possible to find this already implemented for your microcontroller of choice by searching through their application notes or user forums.
(Cypress Semiconductor offers an
implementation of this protocol called EZ I2C for all of their PSoC
microcontroller devices, built into the development tools (see
the Sidebar “Creating
a Custom I2C Device in 15 Minutes.”).
However you accomplish this, the key is to resist reinventing for
each project the protocol or communication management firmware.
If this is your first time and you want to keep the I2C protocol firmware separate from everything else, put the protocol management into a separate task from the rest of the system. For a microcontroller this means attaching it to the I2C interrupt.
Since interrupts need to be handled quickly with minimal system disruption, the interrupt handler will not do any data manipulation or execute any higher-level functions. The interrupt handler will simply take the data off the bus and transfer it to a register or retrieve data from a register and provide it for transmission on the bus.
When the protocol management is handled in a separate task (interrupt) form the main application, the registers look like a dual-port RAM. On one side, the main application is acts upon and updates the data in the registers. On the other side the I2C protocol task transfers data received from the master into the registers and from the registers to the bus for the master. Implementing the protocol management as described above will allow it to be transferred from project to project.
Once the protocol is taken care of, the next concern is the actual data in the registers. The protocol will be asynchronously transferring data in and out of the registers. It is the application’s job to keep the data coherent, so that the data the master sees accurately reflects what is going on.
This can be ensured by making all data 8-bits wide, since I2C transfers are byte-by-byte. But many data elements will not fit into 8-bits and retain the desired resolution. Also, coherency is not just about keeping multiple bytes of a data element in-sync; it means that at no point will a data element contain a anything but its true value (registers are not used for temporary values, such as setting a register to zero and then setting individual status bits true).
Keep all data transfers atomic; if a multi-byte data element is to be updated, disable interrupts around the transfer. When calculations are needed before a register can be determined, use a separate temporary variable or scratchpad and then transfer the final data.
Finally, slaves should never require a master to intervene unless absolutely necessary. The master is the one to decide when it is absolutely necessary, so in your slave design you need to provide interrupt points, but also provide the master the ability to decide not to be interrupted. Provide a mask bit for each status that can cause an interrupt.
A slave should be designed to handle everything that comes up. The emergency interrupt is there to inform the master what has happened, but not to wait for the master to take action. Designing your interrupts this way will further reduce the burden on the master while allowing for a wide range of unusual situations to be handled.
I2C provides a relatively painless approach to creating custom functions that stand-alone and partition a system into a high-level master and lower-level functioning slaves. When Philips created the bus they envisioned system designers building entire devices like computers and televisions by dropping ready-made slave devices onto the 2-wire bus.
While this did not happen, the I2C bus has become an embedded system standard. I2C enabled devices are available as off-the-shelf, drop-in functions, but not to the degree and in the variety required to build-up a complex device.
Microcontroller-implemented custom slave devices can fill this gap and for a company they can provide a portfolio of plug-and-play functions that address the repetitive portion of their designs. Follow the rules and you can successfully harness the I2C bus in your system.
In Part 2 I will address
the test and verification process for I2C in your system. We will
revisit and build upon many of the design considerations and show how
they support and improve the test and verification of an I2C-enabled
Jon Pearson is
the product manager for PSoC development tools at Cypress Semiconductor, located in
Lynnwood, WA. He can be reached at firstname.lastname@example.org.
 The I2C-Bus Specification, Version 2.1. January 2000
 Philips has a page describing the bus and providing links to the official specification and other resources that can be found at http://www/semiconductors.philips.com/products/interfeace_control/i2c/facts
 Cypress Semiconductor offers I2C port expanders and has an application note describing them, AN2305 – Using Cypress I2C port Expanders with Flash Storage, which can be found at http://www.cypress.com by searching on “AN2304”
 Cypress CY8C9520 datasheet (found at http://www.cypress.com ); Philips PCF8574 datasheet (found at http://www.semiconductors.philips.com )