Implementation of I2C communication using bit banging

Implementation of I2C communication using bit banging

Source: www.circuitbasics.com/

Introduction:

The I2C bus was designed by Philips in the early ’80s to allow easy communication between components which reside on the same circuit board. Philips Semiconductors migrated to NXP in 2006.

The name I2C translates into “Inter IC”. Sometimes the bus is called IIC or I²C bus.

The original communication speed was defined with a maximum of 100 kbit per second and many applications don’t require faster transmissions. For those that do there is a 400 kbit fastmode and – since 1998 – a high speed 3.4 Mbit option available. Recently, fast mode plus a transfer rate between this has been specified.  Beyond this, there is the ultra fast mode UFM, but it frankly is no real I2C bus.

I2C is not only used on single boards but also to connect components which are linked via cable. Simplicity and flexibility are key characteristics that make this bus attractive to many applications.

I2C combines the best features of SPI and UARTs. With I2C, you can connect multiple slaves to a single master (like SPI) and you can have multiple masters controlling single, or multiple slaves. This is really useful when you want to have more than one microcontroller logging data to a single memory card or displaying text to a single LCD.

Like UART communication, I2C only uses two wires to transmit data between devices:

SDA (Serial Data) – The line for the master and slave to send and receive data.

SCL (Serial Clock) – The line that carries the clock signal.

I2C is a serial communication protocol, so data is transferred bit by bit along a single wire (the SDA line).

Most significant features include:

  • Only two bus lines are required
  • No strict baud rate requirements like for instance with RS232, the master generates a bus clock
  • Simple master/slave relationships exist between all components
    Each device connected to the bus is software-addressable by a unique address
  • I2C is a true multi-master bus providing arbitration and collision detection

What is bit banging:

In computer engineering and electrical engineering, bit banging is a “term of art” for any method of data transmission that employs software as a substitute for dedicated hardware to generate transmitted signals or process received signals. Software directly sets and samples the states of GPIOs (e.g., pins on a microcontroller), and is responsible for meeting all timing requirements and protocol sequencing of the signals. In contrast to bit banging, dedicated hardware (e.g., UART, SPI, I²C) satisfies these requirements and, if necessary, provides a data buffer to relax software timing requirements. Bit banging can be implemented at very low cost, and is commonly used in some embedded systems.

Bit banging allows a device to implement different protocols with minimal or no hardware changes. In some cases, bit banging is made feasible by newer, faster processors because more recent hardware operates much more quickly than hardware did when standard communications protocols were created. – Wikipedia

Below code snippets demonstrate implementation of I2C communications using general purpose IO

Define the GPIO pins as SCL, SDA, we are using STM32CubeIDE for code generation, user can use/select GPIO pins depending upon their application/microcontroller requirements.

#define SDA_HIGH	HAL_GPIO_WritePin(GPIO_SDA_GPIO_Port, GPIO_SDA_Pin, GPIO_PIN_SET)
#define SDA_LOW		HAL_GPIO_WritePin(GPIO_SDA_GPIO_Port, GPIO_SDA_Pin, GPIO_PIN_RESET)
#define SCL_HIGH	HAL_GPIO_WritePin(GPIO_SCL_GPIO_Port, GPIO_SCL_Pin, GPIO_PIN_SET)
#define SCL_LOW		HAL_GPIO_WritePin(GPIO_SCL_GPIO_Port, GPIO_SCL_Pin, GPIO_PIN_RESET)
#define MI         	HAL_GPIO_ReadPin(GPIO_SDA_GPIO_Port, GPIO_SDA_Pin)

other macro definitions & enumeration declaration

#define I2C_TIMEOUT 100         // 8 mS

#define I2C_BUSY        	0       // i2c status in i2c_status
#define I2C_ERROR       	1
#define I2C_OK          	2
#define BYTE_SIZE_INBITS	8

#define ADDR_24LC1025   	0xA0    // I2C address of EEPROM

enum i2c_states { START , HI_ADDRESS, LOW_ADDRESS, DATA_BYTE_TX , RESTART ,DATA_BYTE_RX, STOP}; // I2C states
enum i2c_operations { READ, WRITE };    // possible I2C operations

Declaring I2C Configuration structure

typedef struct
{
	uint8_t i2c_dev_addr;			//Decive address 
	uint32_t i2c_data_bytes;		//No bytes to be transfer 
	uint32_t i2c_hi_addr;			//Higher address byte
	uint32_t i2c_lo_addr;			//Lower address byte
	uint8_t i2c_state;				
	uint8_t i2c_operation;
	uint8_t i2c_status;
	uint8_t i2c_index;
	uint8_t *pdata;
}I2C_Config;

Start Condition: The SDA line switches from a high voltage level to a low voltage level before the SCL line switches from high to low.

Stop Condition: The SDA line switches from a low voltage level to a high voltage level after the SCL line switches from low to high.

I2C Start & Stop Condition , Source : NXP
void start_i2c (void)
{
	SDA_HIGH;					
	soft_delay_us(5);								// Data Setup Time
	SCL_HIGH;					
	soft_delay_us(5);								// Data Setup Time
	SDA_LOW;					
	soft_delay_us(5);								// Data Setup Time
	SCL_LOW;					
	soft_delay_us(5);								// Data Setup Time
}

void stop_i2c (void)
{
	SDA_LOW;					
	soft_delay_us(5);								// Data Setup Time
	SCL_HIGH;					
	soft_delay_us(5);								// Data Setup Time
	SDA_HIGH;					
	soft_delay_us(5);								// Data Setup Time
	SCL_LOW;					
	soft_delay_us(5);								// Data Setup Time
}

Address Frame: A 7 or 10 bit sequence unique to each slave that identifies the slave when the master wants to talk to it.

Read/Write Bit: A single bit specifying whether the master is sending data to the slave (low voltage level) or requesting data from it (high voltage level).

ACK/NACK Bit: Each frame in a message is followed by an acknowledge/no-acknowledge bit. If an address frame or data frame was successfully received, an ACK bit is returned to the sender from the receiving device.

STEPS OF I2C DATA TRANSMISSION

1. The master sends the start condition to every connected slave by switching the SDA line from a high voltage level to a low voltage level before switching the SCL line from high to low

2. The master sends each slave the 7 or 10 bit address of the slave it wants to communicate with, along with the read/write bit

3. Each slave compares the address sent from the master to its own address. If the address matches, the slave returns an ACK bit by pulling the SDA line low for one bit. If the address from the master does not match the slave’s own address, the slave leaves the SDA line high.

4. The master sends or receives the data frame.

5. After each data frame has been transferred, the receiving device returns another ACK bit to the sender to acknowledge successful receipt of the frame.

6. To stop the data transmission, the master sends a stop condition to the slave by switching SCL high before switching SDA high.

static uint8_t send_i2c (I2C_Config *pI2CConfig, uint8_t tx , char ch)
{
	uint8_t i ;
	uint8_t rx = 0;

	if (ch == 'T')
	{
		for(i=BYTE_SIZE_INBITS; i>0; i--)
		{

			((tx >> (i-1)) & 1) ? (SDA_HIGH) : (SDA_LOW);
			soft_delay_us(5);										// Data Setup Time


			SCL_HIGH;
			soft_delay_us(5);					// Data Setup Time

			SCL_LOW;
			soft_delay_us(5);					// Data Setup Time

		}

		//READING ACK FROM SLAVE

		SDA_HIGH;
		soft_delay_us(5);								// Data Setup Time

		SCL_HIGH;
		soft_delay_us(5);								// Data Setup Time

		if( MI != 0x0 )
		{
			stop_i2c();										// request to transmit a stop condition
			pI2CConfig->i2c_status = HAL_ERROR ;						// Error status
		}
		SCL_LOW;						// PB6 = 0b00		SCL 1=>0
		soft_delay_us(5);									// Data Setup Time

	}

	if (ch == 'R')
	{

		for(i=BYTE_SIZE_INBITS; i>0; i--)
		{

			SCL_HIGH;
			soft_delay_us(5);								// Data Setup Time

			rx <<= 1;
			if( MI == 0x1 )
					{
				rx |= 1;
					}

			SCL_LOW;
			soft_delay_us(5);								// Data Setup Time
		}

		//rcd by ack

		if (pI2CConfig->i2c_index == (pI2CConfig->i2c_data_bytes - 1))
		{
			return rx;
		}

		SDA_LOW;
		soft_delay_us(5);

		SCL_HIGH;
		soft_delay_us(10);					// Data Setup Time

		SCL_LOW;
		soft_delay_us(10);					// Data Setup Time

		SDA_HIGH;		//
		soft_delay_us(5);
	}

	if (ch == 'R')
	{
		return rx;
	}
	else
		return 0;
}
uint8_t read_from_eeprom(I2C_Config *pI2CConfig, uint8_t* p, uint32_t addr,uint32_t count)
{
	uint32_t i;

	// clear read array
	for(i=0; i<count; i++)
		p[i] = 0;

	pI2CConfig->i2c_dev_addr = ADDR_24LC1025;
	pI2CConfig->i2c_data_bytes = count;
	pI2CConfig->i2c_hi_addr = (addr >> 8) & 0xff;       // word Hi-address
	pI2CConfig->i2c_lo_addr = addr & 0xff;              // word Lo-address
	pI2CConfig->pdata = p;

	i2c1_read(pI2CConfig);

	return 0;
}

uint8_t write_to_eeprom(I2C_Config *pI2CConfig, uint8_t* p, uint32_t addr,uint32_t count)
{
	pI2CConfig->i2c_dev_addr = ADDR_24LC1025;
	pI2CConfig->i2c_data_bytes = count;
	pI2CConfig->i2c_hi_addr = (addr >> 8) & 0xff;       // word Hi-address
	pI2CConfig->i2c_lo_addr = addr & 0xff;              // word Lo-address
	pI2CConfig->pdata = p;

	i2c1_write(pI2CConfig);

	soft_delay_ms(50);                 // eeprom byte write time = 10 mS

	return 0;
}
// CAUTION: The global variables i2c_data[], i2c_data_bytes, and i2c_dev_addr
// should be set before calling this function.
static void i2c1_write(I2C_Config *pI2CConfig)
{
	pI2CConfig->i2c_index = 0;              // Initialize index
	pI2CConfig->i2c_status = I2C_BUSY;      // I2C is now busy
	pI2CConfig->i2c_operation = WRITE;      // a write operation is being performed
	pI2CConfig->i2c_state = START;

	start_i2c();
	soft_delay_us(5);

	// i2c_tout = I2C_TIMEOUT;
	while(pI2CConfig->i2c_status == I2C_BUSY)
		i2c1_state_machine(pI2CConfig);    // wait until I2C is no longer busy
}

// CAUTION: The global variables i2c_data[], i2c_data_bytes, and i2c_dev_addr
// should be set before calling this function.
static void i2c1_read(I2C_Config *pI2CConfig)
{
	pI2CConfig->i2c_index = 0;              // Initialize index
	pI2CConfig->i2c_status = I2C_BUSY;      // I2C is now busy
	pI2CConfig->i2c_operation = READ;       // read operation is being performed
	pI2CConfig->i2c_state = START;

	start_i2c();					// places I2C peripheral in  master transmitter mod
	soft_delay_us(5);

	while(pI2CConfig->i2c_status == I2C_BUSY)
		i2c1_state_machine(pI2CConfig);    // wait until I2C is no longer busy
}

Below code snippet for I2C state machine functionality

void i2c1_state_machine(I2C_Config *pI2CConfig)
{
	switch (pI2CConfig->i2c_state)
	{
	case START:

		// (1) START CONDITION TRANSMITTED
		send_i2c(pI2CConfig,(pI2CConfig->i2c_dev_addr & 0xFE) , 'T'); // transmit slave address + write command (SLA + W)
		pI2CConfig->i2c_state = HI_ADDRESS;
		break;

	case HI_ADDRESS:
		send_i2c(pI2CConfig,pI2CConfig->i2c_hi_addr , 'T');        //send higher address
		pI2CConfig->i2c_state = LOW_ADDRESS;          // Set state to DATA_BYTE so that next byte sent is data
		break;

	case LOW_ADDRESS:
		//uart1_printf_debug ("lower add\n");
		send_i2c(pI2CConfig, pI2CConfig->i2c_lo_addr , 'T');     // Transmit the LOW addr
		if(pI2CConfig->i2c_operation == READ)   // if performing a read then the dummy write cycle ends here. no data byte is transmitted
			pI2CConfig->i2c_state = RESTART;
		else
			pI2CConfig->i2c_state = DATA_BYTE_TX;
		break;

	case DATA_BYTE_TX:
		//write operation
		send_i2c(pI2CConfig, pI2CConfig->pdata[pI2CConfig->i2c_index++],'T');   // Transmit the data byte
		if(pI2CConfig->i2c_index >= pI2CConfig->i2c_data_bytes)     // Check for end of data
		{
			stop_i2c();	// request to transmit a stop condition
			pI2CConfig->i2c_status = I2C_OK;	// status of operation is OK
		}
		break;

	case RESTART:
		start_i2c();
		send_i2c(pI2CConfig, (pI2CConfig->i2c_dev_addr | 0x01) , 'T'); // transmit slave address + read command (SLA + R)
		pI2CConfig->i2c_state = DATA_BYTE_RX;
		break;

	case DATA_BYTE_RX:
		//Data Register not Empty (receivers)
		// DATA BYTE RECEIVED - ACK WILL BE TRANSMITTED, Added RB
		if(pI2CConfig->i2c_index < pI2CConfig->i2c_data_bytes)  // Check for array overflow
		{
			pI2CConfig->pdata[pI2CConfig->i2c_index++] = send_i2c(pI2CConfig, 0,'R');       // Assign data read to array
			// (Index starts at 0 while i2c_data_bytes starts at 1)
			if (pI2CConfig->i2c_index >= pI2CConfig->i2c_data_bytes)
			{
				stop_i2c();	// request to transmit a stop condition
				pI2CConfig->i2c_status = I2C_OK;		// Set OK status
			}
		}
		else
		{
			stop_i2c();						// request to transmit a stop condition
			pI2CConfig->i2c_status = I2C_ERROR ;		// Error status
		}
		break;
	}
}
Software Tools:
  1. STM32CubeIDE
  2. STM32CubeMx
  3. Teraterm
Hardware Setup:
  1. STM32G4 Nucleo-64
  2. Mini USB Cable
  3. Jumper wire

Above code snippets is one of the way developer can implement I2C communication using GPIO (bit Banging).

If you enjoyed this article, share your feedback.

References:

1. I2C Org

2. circuitbasics.com

Similar topic:

Implementation of SPI communication using bit banging

If you enjoyed this article, share your feedback.

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: