Khepera III Toolbox/The Toolbox/Modules/i2cal

From Wikibooks, open books for an open world
Jump to navigation Jump to search

The i2cal module provides a very easy to use I2C interface. The I2C bus is the "backbone" of the Khepera III robot, and used to communicate with three microcontrollers:

  • Main microcontroller (infrared sensors, ultrasound sensors, battery)
  • Left motor controller
  • Right motor controller

To work with the sensors and actuators of the robot, you may want to use the khepera3 module which contains high-level functions and itself uses this module to communicate with the microcontrollers. However, if you develop your own extension boards that you stuck on top of the robot, this module offers you all you need to access these boards via the I2C bus.

Synopsis[edit | edit source]

// Initialize the module
i2cal_init();

// Let's assume that the I2C address of your device is 0x45
i2c_address = 0x45;

// Write the 16-bit value 10000 to register 0x82
i2cal_start();
i2cal_writedata_uint8(0x82);
i2cal_writedata_uint16(10000);
i2cal_write(i2c_address);
i2cal_commit();

// Read a 16-bit followed by two 8-bit values (4 bytes total) from register 0x83
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
msg_read = i2cal_read(i2c_address, 4);
i2cal_commit();

first_value = i2cal_readdata_uint16(msg_read, 0);  // read two bytes starting at byte 0
second_value = i2cal_readdata_uint8(msg_read, 2);  // read one byte starting at byte 2
third_value = i2cal_readdata_uint8(msg_read, 3);   // read one byte starting at byte 3

Description[edit | edit source]

After initialisation (i2cal_init), the i2cal module allows to send transactions on the I2C bus. A transaction consists of a sequence of message exchanges, either from the processor to your board (write), or from your board to the processor (read).

Transactions are started with i2cal_start and executed with i2cal_commit. In between these two calls, a series of read and write messages can be added. A write message is added by first writing the data (i2cal_writedata_*), followed by a call to i2cal_write with the device address. Read messages are added with i2cal_read which takes the device address and the number of bytes to read as argument, and returns a pointer to an i2c_msg structure.

The i2cal_commit call transfers the messages and returns after all data has been successfully sent and received. The received bytes are available in the i2c_msg structures, and can be read using the i2cal_readdata_* functions. Note that all data has to read from these structures before starting a new transaction.

Error Handling[edit | edit source]

The i2cal_commit function returns either -1 to indicate success, or 0 to indicate failure. Failure simply means that the data could not be transferred, and is usually caused by one of the following:

  • Error in the firmware of the microcontroller, i.e. the microcontroller does not (or incorrectly) respond to I2C request.
  • Error in the firmware of another microcontroller on the I2C bus, causing it to misbehave (e.g. by pulling SDA or SCL low).
  • Wrong electrical connections, e.g. SDA/SCL flipped, one of the signals grounded, ...

Debugging such errors efficiently requires an oscilloscope (or similar test infrastructure), preferably with support for I2C decoding.

If the electrical design and the firmware implementation on the microcontroller are correct, errors are highly unlikely. Therefore, it is usually not worth implementing a sophisticated error handling procedure.

Transaction Size Limitations[edit | edit source]

The i2cal module limits a transaction to 16 messages (read or write messages combined). Furthermore, the total number of bytes sent must not exceed 256, and the total number of bytes read must not exceed 256 neither. In view of the timing issues discussed in the next section, this should be by far enough for all real-world applications. If necessary (e.g. for testing), the numbers can be changed in i2cal.c.

Timing Issues[edit | edit source]

The I2C bus on the Khepera III robot is running at 100 KHz. Transferring one byte (8 bits + some overhead) over this bus therefore takes about 0.1 ms.

A message consists of an address byte, plus the data to be read or written. As a rule of thumb, the minimum time for a transaction can be calculated as follows:

In reality, two other delays add to this time:

  • Queuing delay: As the bus can only send one message at the time, the kernel keeps a queue of messages and transactions. Your transaction will have to compete with other processes using the I2C bus. To achieve a low delay for all processes, it is preferred to communicate in small transactions (< 100 bytes at a time).
  • Processing delay on the microcontroller: Each transmitted byte has to be acknoledged by the microcontroller, and each received byte had to be prepared by the microcontroller. For the latter, the microcontroller is allowed to buy in up to about 10 ms of time (I2C clock stretch), during which the bus is occupied and your process waiting. To achieve a good bus usage, this should by all means be avoided.

Synchronization Issues[edit | edit source]

A transaction here is called so because it is executed as one atomic block. This is very important, as the following piece of code illustrates:

// Read a 16-bit value from register 0x83 in one transaction -> correct
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
msg_read = i2cal_read(i2c_address, 2);
i2cal_commit();

// Read a 16-bit value from register 0x83 in two transactions -> wrong
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
i2cal_commit();
    // <-- Another process may communicate with the same device here, overwriting your 0x83 request
i2cal_start();
msg_read = i2cal_read(i2c_address, 2);
i2cal_commit();

If only one process is communicating with that device at any given time, both code snippets are equivalent. However, if more than one process is accessing that device (and you should always think this way), splitting the task in two transactions is wrong.

To understand why, put yourself in the position of the microcontroller (or other I2C slave). You receive register 0x83, and prepare your output buffers for the following read operation. Since the transaction ends there, it is possible that another process communicates with you before the first process reads its two bytes. This process may send register 0x92, and you are preparing (and possibly serving) your output buffer for this new request. When the first process finally attempts to read its two bytes, the microcontroller will either serve the first two bytes of the 0x92 request, or serve some error code to indicate that the result of 0x92 has been read already.

As a rule of thumb, each self-contained operation (such as register read, or register write) must be performed as exactly one transaction.

Multi-Threaded Programs[edit | edit source]

Since the data structures of this module are allocated statically, it is not safe to call functions which update the same fields from two different threads. To avoid interference between threads, whole blocks between i2cal_start and i2cal_commit (inclusive) must be synchronized.

Comparison with the Linux I2C Interface[edit | edit source]

Linux (and most other Unix systems) offer a fairly simple interface to the I2C bus via file operations on the I2C device file (/dev/i2c/0), notably ioctl calls. If you look the source code of the i2cal module, you will notice that i2cal_commit is nothing else but such an ioctl call on /dev/i2c/0, and that all other functions around mostly prepare the data structures to pass to that call. Nevertheless, the i2cal module provides an even simpler interface by taking care of all the necessary buffers, and with functions to correctly read and write 2-byte and 4-byte values from these buffers.

API[edit | edit source]

The following list summarizes the functions of the i2cal module:

// Module initialization
int i2cal_init();

// Start a new transaction
void i2cal_start();

// Add general read/write messages
struct i2c_msg *i2cal_read_buffer(int dev, unsigned char *buffer, int len);
struct i2c_msg *i2cal_write_buffer(int dev, unsigned char *buffer, int len);

// Simple read/write, using built-in read/write buffers (use i2cal_writedata_* functions)
struct i2c_msg *i2cal_read(int dev, int len);
struct i2c_msg *i2cal_write(int dev);

// Write data to built-in buffer
unsigned char *i2cal_writedata_uint8(unsigned char value);
unsigned char *i2cal_writedata_int16(int value);
unsigned char *i2cal_writedata_uint16(unsigned int value);
unsigned char *i2cal_writedata_int32(int value);
unsigned char *i2cal_writedata_uint32(unsigned int value);
unsigned char *i2cal_writedata_buffer(int len);

// Commit transaction by sending/receiving data. Returns the ioctl return value, which is negative on error.
int i2cal_commit();

// Read data from result buffer
unsigned char i2cal_readdata_uint8(struct i2c_msg *message, int offset);
int i2cal_readdata_int16(struct i2c_msg *message, int offset);
unsigned int i2cal_readdata_uint16(struct i2c_msg *message, int offset);
int i2cal_readdata_int32(struct i2c_msg *message, int offset);
unsigned int i2cal_readdata_uint32(struct i2c_msg *message, int offset);