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.
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:
- STM32CubeIDE
- STM32CubeMx
- Teraterm
Hardware Setup:
- STM32G4 Nucleo-64
- Mini USB Cable
- 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
Similar topic:
Implementation of SPI communication using bit banging
If you enjoyed this article, share your feedback.