Legacy I2C API

From Nutwiki
Revision as of 17:02, 27 October 2016 by Harald (Talk | contribs) (1 revision imported)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Description

This example shows how to do basic actions over I2C. Please note, that a new API has been presented in Nut/OS 5.0.6. Nevertheless, the legacy API described on this page is still available.

Prerequisites

This example is written on basis of the EIR or AT91SAM7X-EK. Differences to the AVR code are marked. In the following text three access types are named. Reading means retrieving data from another chip. Writing means sending data to another chip. Access means, that the description is valid for reading and writing.


Abstract

The I2C bus is a two wire communication bus. The specifications can be obtained from the NXP (former Philips) website. Just use the search function with the topic "i2c specification".

Basically a data and a clock line are used to clock data in and out of a device connected to the bus. The two lines are open collector lines, pulled high by resistors. I2C is available from 3.3V to 5V. The data line is called SDA, the clock line is SCL. By changing clock before or after the data line, a START or a STOP condition is set. Any data transfer is surrounded by these two conditions. There is a third condition, called RESTART in special situations. I2C is a multi master bus, that allowes any chip on the bus to initiate a communication but most microcontrollers only allow only one type of device at a time, i.e. it can only be master or slave at a time.

Two communications are possible: Read and Write. Any byte transferred must be acknowledged by the receiver. For that reason a 9th bit is following the eight databits.

A device that can initiate communication is called master, a device that can be addressed, but not start communication is called slave. A master does not have an address. To start communication, the master sets up a START condition and then sends out the address of the slave. In the address only 7 bits are address, the 8th bit states read or write for the slave. A logical 1 means read, a logical 0 means write. After that the data can be transmitted. For writing, the master continues to send out data on SDA accompanied by clock pulses on SCL. For reading, the master continues sending out clock pulses on SCL and reading the data coming in on SDA.

Most chips supporting I2C need a bit more complicated setup. These chips, like memory, tuners or even simple port expanders, have several internal registers. To tell the chip, which internal register should be accessed the first byte after the address byte is the IADDR, the internal address inside the chip itself. Any following data written will be treated as data for that register and any data read from the chip is coming from the register addressed before. Most chips support auto increment: If one first addresses a clock chip and with the next byte addresses the hour-register, then switches to read and reads 3 bytes at once, he gets hour-register, minute-register and seconds-register in one cycle. Same goes for memory. If one needs to write 10 consecutive bytes at an address of the memory, setting the address is only needed once. The memory automatically increments the internal address counter (IADDR) with every byte read or written.

Most reads need a write... For addressing an IADDR register, we need to write this first. For this, the RESTART condition is implemented. RESTART is a condition, where inside a normal transmission a START is set without a STOP before. If we want to read data out of a slave from a certain IADDR, we first have to address the slave and write the IADDR register. After that we have to address it again, but with read mode. In order to not interrup the communication for too long, the I2C specification allows issuing a START directly after writing the IADDR value. The slave stays in attention mode, not going to power down or sleep. This might happen if we issue a STOP first which could result in longer wakeup times or it might be internally busy with something else.

ACK and NACK There are two different types of acknowledges (ACK). If a master addresses a slave, he sends out 8 cycles on SCL and 8 bits ( the address) on SDA. After that it sets SDA floating and issues a ninth SCL pulse. On that pulse, the slave pulls SDA low to signal, that it is ready for communication.

This is a very easy mechanism for a master to check, if a slave is ready for access. The master sends out a sequence of START "slave address" periodically and if the slave is internally busy it does not ACK (i.e. NACK) this sequence. This way, the master never forgets to set the STOP condition. After waiting a time, it can try again. If the slave is ready, it will respond with an ACK to the addressing sequence and the master can access the chip. This mechanism is very handy especially with chips that sometimes take an unpredictable amount of time for internal mechanisms, like eeproms (busy while writing from buffer to EEPROM cells) or ADC/DAC (busy while sampling and converting).

The second ACK cycle follows after every transmitted byte. While the addressing ACK is always set by the slave chip, the data ACK is always set by the reader. If one needs to read from a memory chip we have all ACK conditions: Master writes slave address with R/W-Bit low, slave responds with ACK Master writes IADDR byte(s), slave responds to every byte with ACK Master sets RESTART and sends slaves address with R/W-bit high, slave responds with ACK Master clocks in data bytes and sets ACK on every byte received.

Source Code

Initialization

As the I2C bus does not need any secondary chip selects, the setup is very easy.

<source lang="c"> u_long baud = 115200;

/*

* Register the UART device, open it, assign stdout to it and set
* the baudrate.
*/

NutRegisterDevice(&DEV_DEBUG, 0, 0); freopen(DEV_DEBUG_NAME, "w", stdout); _ioctl(_fileno(stdout), UART_SETSPEED, &baud);

printf( "\n*** EEPROM Test ***\n");

/*

* Register the I2C device, open it and set the baudrate.
*/

baud = 100000; TwInit( 0 ); /* par = slave address but we are master */ TwIOCtl( TWI_SETSPEED, &baud); EEInit(); </source> This part initializes the controller like anyone would do it. I2C related is the part starting with baud = 100000; In this case we reuse the variable for baudrate programming of the USART to setup the speed of the I2C communication. After that we call TwInit( 0); 0 means we are master. Any other value would setup our I2C hardware to be a slave on the bus at the given address.

Like USART or SPI, the TwIOCtl() call sets up special parameters, in this case the speed.

Device Access

Accessing a device is done through 3 routines:
TwMasterTransact() Reading and writing through one routine
TwMasterRead() Reading with setup of IADDR if needed
TwMasterWrite() Writing with setup of IADDR if needed
Attention: The actual AVR code does only support the TwMasterTransact.

The negative aspect of the IADDR addressing, the setup of an internal address register in the slave, is that in most cases we have to send a small buffer out to the slave, before we can read or write. So we have to install a small buffer for every transaction or we have to reuse a buffer. All in all it is a bit more complicated than it has to be. TwMasterTransact always sends before read. So we can use a single buffer for sending the IADDR out first and read back the addressed registers or memory.

It is much easier with TwMasterRead() and TwMasterWrite(). Here we can pass the IADDR and its size as parameters. This is much more comfortable as these parameters can be variables set up by driver software or anything else. As a secondary, a buffer can be preset with values allowing to detect if a transaction went wrong. The following example initializes a small io-expander PCA9555 that has two 8-bit ports A and B. It can be found in nut/dev/pca9555.c <source lang="c">

/*****************************************************************/ int IOExpInit( void ) /*****************************************************************/ {

   uint8_t cmd[2];
   cmd[0] = 0x0f;  /* port 0 input   */
   cmd[1] = 0x00;  /* port 1 output  */
   if( TwMasterRegWrite( I2C_SLA_IOEXP, 0x06, 1, cmd, 2, 50 ) == -1 )
   {
       // printf( "Init Chip not responding\n");
       return -1;
   }
   cmd[0] = 0xff;  /* 4 polarity reg 0 */
   if( TwMasterRegWrite( I2C_SLA_IOEXP, 0x04, 1, cmd, 1, 50 ) == -1 )
   {
       // printf( "Init Chip not responding\n");
       return -1;
   }
   cmd[0] = 0xf0;  /* 4 polarity reg 1 */
   if( TwMasterRegWrite( I2C_SLA_IOEXP, 0x02, 1, cmd, 1, 50 ) == -1 )
   {
       // printf( "Init Chip not responding\n");
       return -1;
   }
   return 0;

}

/*****************************************************************/ int IOExpRawWrite ( uint8_t port, uint8_t value ) /*****************************************************************/ {

   if ( port > 1 )	return -1;
   portValue[port] = value;
   if( TwMasterRegWrite( I2C_SLA_IOEXP, 0x02 + port, 1, &portValue[port], 1, 50 ) == -1 )
       return -1;
   return 0;

}

/*****************************************************************/ int IOExpRawRead ( uint8_t port, uint8_t *value ) /*****************************************************************/ {

   if( port > 1 ) return -1;
   if( TwMasterRegRead( I2C_SLA_IOEXP, port, 1, value, 1, 50) == -1)
       return -1;
   return 0;

}

</source> The second two functions show raw access of any of the ports in the PCA9555. It demonstrates the easier handling of IADDR (port) without introducing a helper-buffer as it would be necessary by TwMasterTransact(). Unfortunately the handling of IADDR is a specialty provided by the hardware i2c part of the SAM7 CPUs. It is not available in most of the AVR derivates.

Authors Comment

I plan to do a software implementation of TwMasterRead() and TwMasterWrite() for AVR on my next pending project based on AT90CAN128.

See also