Tuesday, October 19, 2021

I2C and TWI (Two Wire Interface) library for AVR microcontrollers

The last article was about How I2C and TWI protocol works and we saw that they are mostly the same so this library works for both I2C and TWI serial interfaces. You don't have to know every detail about how the I2C protocol works but I strongly recommend reading the article to have a general idea about it, and that way it will be easier to use this library.


Contents


Two-wire serial Interface

The TWI protocol allows the systems designer to interconnect up to 128 individually addressable devices using only two bi-directional bus lines - one for clock (SCL) and one for data (SDA). The only external hardware required to implement the bus is a single pull-up resistor for each of the TWI bus lines. All devices connected to the bus have individual addresses, and mechanisms for resolving bus contention are inherent in the TWI protocol.

TWI Bus Interconnection

Both TWI lines (SDA and SCL) are bi-directional, therefore outputs connected to the TWI bus must be of an open-drain or an open-collector type. Each line must be connected to the supply voltage via a pull-up resistor. A line is then logic high when none of the connected devices drives the line, and logic low if one or more drives the line low.

The pull-up resistor is typically chosen between 1 kΩ and 10 kΩ ranges for Standard and Fast modes, and less than 1 kΩ for High-Speed mode. Note that the internal pull-ups in the AVR pads can be enabled by setting the PORT bits corresponding to the SCL and SDA pins, as explained in the I/O Port section. In some systems, the internal pull-ups can eliminate the need for external resistors.


API


Structure objects

Since some micro-controllers have two TWI modules, every function takes a pointer to a structure object as an argument. This way, both TWI modules can be used at the same time.

Example:

TWI_Init(&twi0, TWI_400KHZ)

Notice the ampersand used to pass the memory address as an argument.

There are two structure objects defined that can be used: twi0 and twi1. By default only twi0 module is included. If you need the twi1 module, the TWI_ENABLE_TWI1 define must be set to 1 in the header file.

TWI initialization

void TWI_Init(TWI_t* twi, uint32_t frequency)

Only for Master modes. Used to initialize the TWI module. This will set the TWI bit rate and enable global  interrupts. 400kHz is the maximum TWI speed that regular AVR microcontrollers supports although I have managed to talk to a DAC at 577kHz.

frequency:

Can be one of the following constants (TWI_100KHZ, TWI_400KHZ) or any value between 100-400kHz. 

According to AVR311:

Slave operation does not depend on Bit Rate or Prescaler settings, but the CPU clock frequency in the Slave must be at least 16 times higher than the SCL frequency. The following table shows minimum CPU clock speeds for normal and high speed TWI transmission.

TWI Slave CPU vs Master TWI Clock
AVR311

Set address

void TWI_SetAddress(TWI_t* twi, uint8_t addr, bool general_call)

Set device address. The address '0000 000' is reserved for a general call. The Address Match unit is able to compare addresses even when the AVR MCU is in sleep mode, enabling the MCU to wake up if addressed by a Master.

addr:

A 7 bit address starting from 1.

general_call:

If true, the first bit of TWAR register will be set to 1 and the TWI module will respond to the general call address 0x00.

Start transmission

void TWI_StartTransmission(TWI_t* twi)

This function sends the START command to begin the transmission and so putting the microcontroller in a Master mode. If an error occurs the TWI_CHECK_STATUS is set. The returned status code from TWI hardware is saved in the TWI_STATUS_CODE variable. After this function TWI_ContactDevice() should be used.

After a repeated START condition (status code 0x10), the 2-wire Serial Interface can access the same Slave again, or a new Slave without transmitting a STOP condition. Repeated START enables the Master to switch between Slaves, Master Transmitter mode and Master Receiver mode without losing control of the bus.

The START/STOP controller is able to detect START and STOP conditions even when the AVR MCU is in one of the sleep modes, enabling the MCU to wake up if addressed by a Master.

Send TWI address

void TWI_ContactDevice(TWI_t* twi, uint8_t address, uint8_t rw)

Sends the SLA+RW packet  (7-bit address and read or write bit).

address:

The address of the device to communicate with.

rw:

Read or write mode. In read mode the TWI interrupt will be enabled and the received data  can be accessed using TWI_ReadByte().
Can be one of the following constants: TWI_READ_MODE, TWI_WRITE_MODE.

Transmit a byte

void TWI_TransmitByte(TWI_t* twi, uint8_t byte_data)

Transmit a single byte.

byte_data:

The byte to send.

Transmit a string of bytes

void TWI_Transmit(TWI_t* twi, const uint8_t *data)

Transmit a string of bytes. If the receiver sends a NACK the rest of the bytes, if any, will not be transmitted. If status code is not ACK the error flag will be set and rest of bytes will not be transmitted. The last array element must be a NULL.

data:

Pointer to a null terminated array.

Read byte

uint8_t TWI_ReadByte(TWI_t* twi)

When TWI_ContactDevice() is used in read mode, this function is used to return a received byte. If no byte is available it will return null. The received bytes will be stored by the TWI ISR in a circular buffer and each time this function is executed the next byte will be returned.

Byte ready

bool TWI_ByteReady(TWI_t* twi)

Returns true if new bytes are available. When this returns true, the read byte function can be used.

Stop transmission

void TWI_StopTransmission(TWI_t* twi)

Only for Master modes. Issue a STOP command to end the TWI transmission.

Set Slave mode

void TWI_SlaveMode(TWI_t* twi)

Enable TWI, address acknowledgment and interrupt to receive data. The TWI waits until it is addressed by its own slave address (or the general call address if enabled) followed by the data direction bit. If the direction bit is 1 (read), the TWI will operate in ST mode, otherwise SR mode is entered. The ST mode may also be entered if arbitration is lost while the TWI is in the Master mode (see state 0xB0).

Slave transmitter mode

bool TWI_DataRequest(TWI_t* twi)

Returns true if a master asks for data using SLA+R. Set by interrupt on TWI_CODE_ST_SLA_ACK.

Disable TWI

void TWI_Disable(TWI_t* twi)

Disables the TWI. Any ongoing transmissions will be stopped immediately. Using TWI_StartTransmission() will re-enable the TWI module.

Read error flag

uint8_t TWI_StatusNotACK(TWI_t* twi)

Returns the error flag [0:1] in case the TWI status code is other than ACK.

Read status code

uint8_t TWI_ReadStatusCode(TWI_t* twi)

Returns the last TWI status code. Can be used when the error flag is 1. The TWI status codes are defined in the header file.

Reset TWI interface

void TWI_ResetTWIInterface(TWI_t* twi)

Resets the TWI interface by sending a START, 9 of 1's another START and a STOP, in case an error appeared on the bus. The explanation for this is a bit complex and can be found is some data sheets for example the data sheet for MCP4706 DAC page 70.

For this function to work, the TWI_USE_INTERFACE_RESET define must be set to 1 and then below it, set the port and pins that correspond to the TWI module (0 or 1).

Error flag and status codes

After a function is executed, if the returned status code from the TWI module is not ACK, the TWI_CHECK_STATUS flag will be set to 1. The flag will be cleared automatically. It's up to the software designer to decide what actions to take based on the status code.

The same functions will also save the status codes from the TWI module, in the TWI_STATUS_CODE variable. A list with TWI status codes and what they represent can be found inside the header file in the status codes section.

Code example - Master Transmitter (MT) mode - Transmit data to an I2C or TWI device

uint8_t data_array[] = {123, 67, '\0'}; // last byte must be a NULL
uint8_t status_code;
uint8_t device_address = 0x03;

// Initialize TWI 0 at 100kHz
TWI_Init(&twi0, TWI_100KHZ);
	
// Start transmission. This enables the Master mode.
TWI_StartTransmission(&twi0);
	
// Optional. Do something based on TWI status code.
if(TWI_StatusNotACK(&twi0)){
    status_code = TWI_ReadStatusCode(&twi0);
}
	
// Select the device using it's address and set the write mode
TWI_ContactDevice(&twi0, device_address, TWI_WRITE_MODE);
	
// Optional. Do something based on TWI status code.
// If device doesn't respond with ACK, send a STOP.
if(TWI_StatusNotACK(&twi0)){
    status_code = TWI_ReadStatusCode(&twi0);
}
	
// Transmit an array of bytes
TWI_Transmit(&twi0, data_array);
	
// ... the transmit function can be used again to transmit 
// as many bytes is necessary
	
// Optional. Do something based on TWI status code.
// If device doesn't respond with ACK after every byte, send a STOP.
if(TWI_StatusNotACK(&twi0)){
    status_code = TWI_ReadStatusCode(&twi0);
}
	
// Stop the transmission
TWI_StopTransmission(&twi0);


Code example - Master Receiver (MR) mode - Read data from an I2C or TWI device

int main(void){
    #define array_size	5
    uint8_t received_data[array_size] = {0};
    uint8_t idx = 0;
    uint8_t status_code;
    uint8_t device_address = 0x03;

    // Initialize TWI 0 at 100kHz
    TWI_Init(&twi0, TWI_100KHZ);
	
    // Start transmission. This enables the Master mode.
    TWI_StartTransmission(&twi0);
	
    // Select the device using it's address and set the write mode
    // to instruct the device about the type of data.
    TWI_ContactDevice(&twi0, device_address, TWI_WRITE_MODE);
	
    // Transmit a code just as an example.
    TWI_TransmitByte(&twi0, 0x21);
	
    TWI_StartTransmission(&twi0);
    TWI_ContactDevice(&twi0, device_address, TWI_READ_MODE);
	
    while(1){
	if(TWI_ByteReady(&twi0)){
	    received_data[idx] = TWI_ReadByte(&twi0);
	    idx++;
	    if(idx >= array_size) idx = 0;
	}
		
	status_code = TWI_ReadStatusCode(&twi0);
		
	// Issue a STOP if 2 bytes were received.
	if((status_code == TWI_CODE_MR_DATA_IN_NACK) || (idx > 1)){
	    // Stop the transmission
	    TWI_StopTransmission(&twi0);
	}
		
    }
	
    return(EXIT_SUCCESS);
}
 

Slave Receiver (SR) mode

int main(void){
    #define array_size	5
    uint8_t received_data[array_size] = {0};
    uint8_t idx = 0;
    const uint8_t device_addr = 0x03;
	
    // Initialize TWI0 in Slave mode and set the address
    TWI_SetAddress(&twi0, device_addr, 0);
    TWI_SlaveMode(&twi0);
	
    while(1){
	if(TWI_ByteReady(&twi0)){
	    received_data[idx] = TWI_ReadByte(&twi0);
	    idx++;
	    if(idx >= array_size) idx = 0;
	}
    }
	
    return 0;
}

Slave Transmitter (ST) mode

When the device is addressed by a Master using SLA+R mode, the interrupt will trigger and the software application will be informed using the DataRequest() function. In the interrupt at that point, the interrupt will be deactivated otherwise the same byte in the TWDR register will be sent continuously. The data is not transmitted using the interrupt, but by using the transmit function. Even disabling the interrupt when the address is acknowledged, a byte is still transmitted by the TWI hardware expecting the TWDR to be written in the interrupt. When the TWDR is not written the byte that will be transmitted is always 7. Don't know why. This makes that the first byte in the array is not transmitted and for this reason the first array value is duplicated. Rest of the bytes are sent correctly.

Having the data prepared when is requested is only feasible if only one type of data is needed otherwise there is no time to prepare the data between the address acknowledgement and first data byte.

int main(void){
    uint8_t arr[] = {123, 123, 67, 99, 88, '\0'};

    // Initialize TWI0 in Slave mode and set the address
    TWI_SetAddress(&twi0, device_addr, 0);
    TWI_SlaveMode(&twi0);
	
    while(1){
	if(TWI_DataRequest(&twi0)){
	    // The STOP must be issued by the Master.
	    TWI_Transmit(&twi0, arr);
	}
    }
	
    return 0;
}

Download

v3.0
twi.h
twi.c
v2.0
twi.h
twi.c
Changelog
v3.0 (9-04-2025) - Added support for Slave mode.
- Fixed some issues with status codes.
v2.0 (1-01-2025) - Now devices with multiple TWI modules can use them simultaneously by using object pointers as function parameters.
- Some code optimization.
v1.2 - Fixed a bug on ATmega328P and similar devices with only one I2C module where registers TWxRn are not defined.

No comments:

Post a Comment