In this example, the 10 bytes of data are
defined with a structure. The application
may use these variables just as it would any
other local or global variables. If the structure
is defined as global during compile
time, most compilers will flatten it out so
that it doesn’t have to calculate the offset
each time an element is referenced. In
other words, there will be no code penalty
for using such a structure.
Implementation
Now that the interface between the master
and slave is designed, it’s time to write
some code. Given the availability of I2C in
a wide range of capable microprocessors,
many vendors also supply I2C-friendly
development tools and libraries. You’ll still
need to write some of your own code, but
these will accelerate your development. For
example, Cypress PSoC microcontrollers
contain low-level I2C hardware that can
be customized using PSoC Designer and
application-specific EzI2C user modules.
Other than basic hardware setup commands,
like I2C_Start( ) and I2C_Stop( )
that enable and disable the interface, the
bulk of the code will be implemented in the
Interrupt Service Routine (ISR). The lowlevel
I2C hardware understands I2C bus
Start and Stop conditions and sets a status
flag when the slave address and R/W bit
are received. It doesn’t check for an address
match, but requires the firmware to perform
that task.
The flow chart shows the basic firmware
flow (Fig. 6). Note that some hardware
details specific to the manufacturers hardware
aren’t covered in the flowchart.
For many applications in which each
byte is independent of the other, this interface
works well. A good example can be
seen in the example application (See code listing 1).Each of the three bytes is
independent of each other.
This example consists of an 8-pin PSoC
CY8C27143-24PXI microcontroller, two
LEDs, two current-limiting resistors for
the LEDs, a pushbutton, and a potentiometer
to simulate a variable voltage.
Internally, the following components are
instantiated: ADC, PGA, two LED drivers,
and the EzI2C’s User Module. The
code in the listing is the only firmware the
user must write for this application. The
I2C interface code is handled in the ISR as
discussed. The I2C master can monitor the
ADC value, check the switch status, and
set the state of the LEDs in this application.
This interface can be reused across
many projects without having to modify
the interface again.
Figure 7 shows the memory representation
down to the actual memory locations.
The project used 1076 bytes of flash and
19 bytes of RAM. The I2C code comes to about 275 bytes, well under the 512 bytes
allotted for this interface.
Some applications require handshaking
between the master and slave instead
of just anonymous data read and writes.
Extending this interface to perform handshaking
is a minor addition to the master
and slave/application code. There are many
ways to add this functionality.
For instance, if an ADC result is more
than 8 bits, it would be possible for the
host to read the MSB of one ADC conversion
and the LSB of the next conversion.
If the readings are very stable, you
might not get into trouble. But if the result
is between two values, for example 0x0200
and 0x01FF, you could accidentally get a
reading of 0x02FF.
To avoid this, we can add a command
byte or semaphore. Listing 2 shows a modified structure
from the previous example. An additional
element has been added to the structure
“bCMD,” and the ADC result variable was
changed from an 8-bit value “bADC” to a
16-bit value “iADC.”
Now instead of the peripheral firmware
blindly updating the ADC result, it waits
for a command or semaphore from the
master. The command could be any nonzero
value of bCMD, or bCMD could be
a wide range of commands that the slave/
peripheral can perform. To keep it simple,
the LEDs and the switches will continue
to update constantly. The iADC value, on
the other hand, will only update when the
bCMD value is set to a non-zero value.
The application now monitors bCMD,
and when it is non-zero, it will put the
latest ADC result in iADC and then set
bCMD to zero. The master will then monitor
bCMD and only retrieve iADC when
bCMD returns to zero. In this way, the
master will never get an ADC result that’s
out of sync. The rule for the command/
semaphore is that the master may set it,
and the slave can only clear it. This is the
implementation of the top layer “Optional
Command Protocol” discussed previously.
There’s no need to make it any more
complicated than that (Listing 3).
The big hurdle in developing such an
interface is writing the I2C driver code in
first place. The driver in this case was written
in M8C assembly language. I’d rather
use C, but at the time and with the tools
available, it was the best way to guarantee
fast and efficient code. This interface works
for most I2C slave applications.
Once the driver was written, I found I
could create a new custom peripheral in
under an hour. This has been extremely
useful in quickly implementing runtime
debugging. Variables can be monitored
with an I2C master while the slave code is
running.