Friday, December 12, 2008

i2c master

Do you want to read an i2c compass? Store data into an i2c eeprom? Jallib let your PIC act as an i2c master and provides a powerfull and clean interface. And it's easy to use.
After the blink example, follow the steps:

Hardware setup.
First you have to setup the i2c bus hardware.
The first thing to concider is if you can and want to use the MSSP to handle i2c (i2c hardware) or handle i2c in software.
The first option - i2c hardware - is possible for most (but not all) PICs with MSSP. The advantage is that this option is generally faster then software i2c. If you choose i2c hardware, you will use the pre-defined i2c clock and i2c data pins. (scl and sda).
With i2c in software, pick any free io pin for i2c clock and i2c data.

Now you have choosen the pins, you need to setup the hardware. Connect clock, data and ground from the PIC to the i2c slave, place pull-up resistors (e.g. 1k5) from both clock and data to the pic power supply and power the slave device. For more details on this, check the i2c slave datasheet or look on the Internet.

Software setup
Now you have the hardware ready, you need to configure the software.

First, define the pins used, for example on an 16F877a:
var volatile bit i2c_scl is pin_c3
var volatile bit i2c_scl_direction is pin_c3_direction
var volatile bit i2c_sda is pin_c4
var volatile bit i2c_sda_direction is pin_c4_direction

Second, define the next two constants:
const word _i2c_bus_speed = 1 ; * 100kHz
const bit _i2c_level = true ; i2c levels (not SMB)

And now you can include the 'level0' i2c library. This is the library that creates the i2c signals and comes in two flavors:

include i2c_software


include i2c_hardware

Both libraries have the same interface to send and receive bytes. You can build complex messages with this interface but in most cases, it is easier to use the level1 layer. To use this, define an array for transmit and an array for receive and include the library:

var byte i2c_tx_buffer[6]
var byte i2c_rx_buffer[10]
include i2c_level1

The size of the two buffers are the maximum size of a message you will send or receive. When a buffer is too short, you might get compile errors or - worse - unexpected behavior. When the buffers are too long, memory will be exhausted faster. When in doubt: enlarge the buffer a few bytes!

Read from an i2c eeprom
Now we are ready to communicate with the i2c slave. The format of the messages depend on the device, so you should consult the slave's documentation. There is an example below of communicating with an i2c eeprom (24lc256).
First thing to know of a slave is it's address. An i2c address is 7 bits and is stored in the higher 7 bits of a byte. The lowest bit is set to zero. The i2c eeprom address is 0xA0 (160 decimal).
Now we want to read data from the eeprom. Let's assume we want to start at internal (eeprom) location 1234 and need 3 bytes. From the 24lc256 datasheet, we learn that we have to write a 2-byte (= one word) location. Subsequently, we can read data. Or in JAL:

-- Send word location to device 0xA0 and read 3 bytes data
r = i2c_receive_wordaddr(0xA0, 1234, 3)

print_byte_hex(serial_hw_data, i2c_rx_buffer[0]);
serial_hw_data = " "
print_byte_hex(serial_hw_data, i2c_rx_buffer[1]);
serial_hw_data = " "
print_byte_hex(serial_hw_data, i2c_rx_buffer[2]);

So only one call to i2c_receive_wordaddr to do the i2c write and read! This function takes 3 params: the i2c slave device address, the value of the 2-byte location code and the number of byte to read after the location code is sent.
The result is stored in i2c_rx_buffer[] and is printed on the serial port by the example. (Have a look at 'print_serial_numbers.jal' for more details on the use of serial comms.)
You might have notices the return value r in the sample above. This bit value is true if the operation was succesfull. If false, communication has failed. Add code to check the return value and handle errors in your program!

i2c_receive_byteaddr is a similar function which uses a 1-byte location code.

Arbitrary location code lengths can be sent by using i2c_send_receive. This function is used in the example below.

Write to an i2c eeprom
In most cases, you also want to write to the eeprom. This is how you write value 99 to location 2 of the eeprom:

-- write part (increment 3rd byte at 0x0002)
i2c_tx_buffer[0] = 0 -- high byte location in i2c eeprom
i2c_tx_buffer[1] = 2 -- low byte location in i2c eeprom
i2c_tx_buffer[2] = 99 -- data
r = i2c_send_receive(0xA0, 3, 0)

Function i2c_send_receive takes the slave address as the first parameter. The second one defines the number of bytes to be sent to the slave from i2c_tx_buffer. The third param defines the number of bytes to be received from the slave (and stored in i2c_rx_buffer). In the example above, we don't want to receive any information so the third param is 0 and i2c_rx_buffer is not used.

How to continue.
We've seen how to setup an i2c master, either using hardware or software. And we read data from an i2c eeprom and wrote a byte to it. The test-program test_i2c_sw_l1.jal, preceded by a board file like board_16f877a_dwarf.jal, provides a working example for the code shown. And in the near future, we intend to add samples of i2c code to jallib that are ready to compile. Adapt it to your specific slave and off you go!
And if you can't get it to work, spent 2 euro on an 24lc256 as a 'i2c reference device', so you can check if your PIC and it's program are working like they should.

Joep Suijs

1 comment:

  1. Thanks for this, Joep. The level1 i2c API seems nice, with its buffers.

    Some thoughts: IIRC addresses are 7-bit coded, + 1-bit to specify if the address is for reading or writing. This can sometimes be confusing, when getting the right slave's address.

    (I've added labels to your post. "i2c" and "confirmed": what this is about, and for who.