Writing a simple I2C library
Objective
I recently learned about I2C communication protocol and I wanted to implement a simple library for it that I could use in future projects.
Background
I2C or Two Wire Interface (TWI) is a synchronous, Controller-Target, communication protocol. It only requires two wires, one for clock (SCL) and second for data (SDA).
Each device has a 7-bit address, allowing for a maximum of 127 devices.
Communication takes place in form of packets which are marked by a START and a STOP condition. To communicate with a specific target, controller sends a START condition followed by an address byte, which consists of a 7-bit target address and a read/write bit.
A repeated START condition could be sent without a STOP condition to address a different target without relinquishing the control of the bus.
See Example for more details.
For starters, I am going to need the following functions:
- Initialize I2C
- Start the communication
- Stop the commmunication
- Send a byte
- Receive a byte and send ACK
- Receive a byte and send NACK to signal end of transmission
- Read status register
Code
-
// myI2C.c #include <avr/io.h> #include "myI2C.h" void initI2C(void) { TWBR = BIT_RATE_REGISTER; TWCR |= 1 << TWEN; }
// myI2C.h #include <avr/io.h> #define I2C_W 0x00 #define I2C_R 0x01 #define BIT_RATE_REGISTER 32 //p.222 datasheet void initI2C(void); void i2cWaitForComplete(void); void i2cStart(void); void i2cStop(void); void i2cSend(uint8_t data); uint8_t i2cReadAck(void); uint8_t i2cReadNoAck(void); uint8_t readStatus(void);
Register and pin names such as
TWBR
,TWCR
,TWEN
come fromavr/io.h
.BIT_RATE_REGISTER
macro is frommyI2C.h
TWBR
is the bit rate register. I2C module on AVR supports upto 400 KHz but I am using 100 KHz correspoding toTWBR
value of 32 set in the header file.TWEN
needs to be set inTWCR
to enable I2C. -
// myI2C.c void i2cWaitForComplete(void) { loop_until_bit_is_set(TWCR, TWINT); }
This function uses a blocking loop until
TWINT
inTWCR
is set.TWINT
is interrupt flag and it is set when I2C module finishes a task. -
void i2cStart(void) { TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWSTA); i2cWaitForComplete(); }
Set START condition (
TWSTA
) and make sure I2C is enabled (TWEN
) in the Control Register (TWCR
). SettingTWINT
bit to 1 clears it and starts the communication.The waiting loop
i2cWaitForComplete()
waits until this task is finished. -
void i2cStop(void) { TWCR = ((1 << TWEN) | (1 << TWINT) | (1 << TWSTO)); }
Similar to
i2cStart()
but setting the STOP bit (TWSTO
). This marks the end of the transmission until the next START condition. -
void i2cSend(uint8_t data) { TWDR = data; TWCR = (1 << TWEN) | (1 << TWINT); i2cWaitForComplete(); }
TWDR
is the Data Register and we store thedata
byte in it. ThenTWINT
is set again to start the process. -
uint8_t i2cReadAck(void) { TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA); i2cWaitForComplete(); return (TWDR); }
When addressing a target in Read mode, this function is used to set
TWINT
andTWEA
. SettingTWEA
make the controller sends theACK
condition to acknowledge the receive.After the waiting loop finishes, byte from the Data Register is returned.
-
uint8_t i2cReadNoAck(void) { TWCR = (1 << TWEN) | (1 << TWINT); i2cWaitForComplete(); return (TWDR); }
Similar to
i2CReadNoAck()
butTWEA
is not set. As a result,NACK
instead ofACK
is sent and target does not put new data in the data register. -
uint8_t readStatus(void) { return (TWSR & 0xF8); }
TWSR is the Status Register and this returns the Status code masking the bits [2:0] that are not the part of status code
Complete Code
#include <avr/io.h>
#include "myI2C.h"
void initI2C(void) {
TWBR = BIT_RATE_REGISTER;
TWCR |= 1 << TWEN;
}
void i2cWaitForComplete(void) {
loop_until_bit_is_set(TWCR, TWINT);
}
void i2cStart(void) {
TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWSTA);
i2cWaitForComplete();
}
void i2cStop(void) {
TWCR = ((1 << TWEN) | (1 << TWINT) | (1 << TWSTO));
}
void i2cSend(uint8_t data) {
TWDR = data;
TWCR = (1 << TWEN) | (1 << TWINT);
i2cWaitForComplete();
}
uint8_t i2cReadAck(void) {
TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
i2cWaitForComplete();
return (TWDR);
}
uint8_t i2cReadNoAck(void) {
TWCR = (1 << TWEN) | (1 << TWINT);
i2cWaitForComplete();
return (TWDR);
}
uint8_t readStatus(void) {
return (TWSR & 0xF8);
}
Example
We will use myI2C lib to read temperature data from the MPU6050 sensor and display it over UART. The code is pretty self-explanatory except for the I2C part which I explained below.
i2cStart();
i2cSend(MPU_ADDRESS + I2C_W);
i2cSend(PWR_MGMT_1);
i2cSend(0x00);
i2cStop();
We start by sending a START condition to signal packet start.
Then we send an address byte which consists of 7-bit MPU_ADDRESS
and one write bit I2C_W
.
This tells the target that we wish to communicate with it in WRITE mode.
Then, we write the address of the Power Management Register.
In the end, we write the value to be stored in Power Management Register followed by STOP.
while (1) {
// Packet start
i2cStart();
i2cSend(MPU_ADDRESS + I2C_W);
i2cSend(TMP_REGISTER);
i2cStart();
i2cSend(MPU_ADDRESS + I2C_R);
temp = (i2cReadAck() << 8) + i2cReadNoAck();
i2cStop();
In this portion of the code, we read values from the Temperature Register (TMP_REGISTER
) on the target using a while loop.
We begin as before, with a START followed by addressing the target in write mode, and writing the address of the register we are interested in.
To read values from TMP_REGISTER
, we need to address the target in READ mode. This is done by sending another START followed by the address byte with R/W bit set.
Then we read a byte using i2cReadAck()
, shift it 8 bits to the left before reading another byte using i2cReadNoAck()
. The target automatically increments the register to be read after a read.
We finish with a STOP to signal end of packet.
Here is the full code:
#include <avr/io.h>
#include <util/delay.h>
#include "../uart/myUART.h"
#include "myI2C.h"
#include <stdio.h>
// Store addresses
#define MPU_ADDRESS 0xD0
#define PWR_MGMT_1 0x6B
#define TMP_REGISTER 0x41
int main(void) {
int16_t temp;
char *string_for_output;
initUART(9600);
printString("UART Initialized\r\n");
initI2C();
printString("I2C Initialized\r\n");
// First packet starts
i2cStart();
i2cSend(MPU_ADDRESS + I2C_W);
i2cSend(PWR_MGMT_1);
i2cSend(0x00);
i2cStop();
printString("Begin reading...\r\n");
while (1) {
// Packet start
i2cStart();
i2cSend(MPU_ADDRESS + I2C_W);
i2cSend(TMP_REGISTER);
i2cStart();
i2cSend(MPU_ADDRESS + I2C_R);
temp = (i2cReadAck() << 8) + i2cReadNoAck();
i2cStop();
// Packet end
sprintf(string_for_output, "%f", (temp / 340.0) + 36.53);
printString(string_for_output);
printString("\r\n");
_delay_ms(100);
}
}