11. I2C
11.1 What Is I2C?
IIC, Inter-Integrated Circuit, also called the I2C bus, is a serial communication protocol usually used to connect low-speed peripherals. It was developed by Philips, now NXP Semiconductors, in the early 1980s and has become a standard. The IIC bus requires only two data lines: serial data line, SDA, and serial clock line, SCL. This makes it a very simple interface. It is suitable for chip-based communication, such as connecting sensors, memory, digital signal processors, and so on. In the IIC protocol, there is one master device and multiple slave devices on the bus. The master controls communication on the bus and is responsible for initiating, controlling, and stopping communication. Slave devices wait for requests from the master and then receive or send data. Data exchange between the master and slaves uses a frame format. Each frame usually contains address, data, and control information. The master selects the device to communicate with according to the slave address, and the slave performs the corresponding operation according to the control information. The IIC protocol supports multiple slave devices connected to the same master, providing greater flexibility for system design.
11.2 I2C Hardware Implementation
The I2C bus usually uses two voltage levels: high level, VH, and low level, VL. The high level is 2.5 V to 5.5 V, and the low level is 0 V to 0.3 V. These voltage level ranges are defined by the I2C specification. I2C buses support different transfer rates, including standard mode, 100 kbps, fast mode, 400 kbps, and high-speed mode. The transfer rate is selected according to application requirements and device support. To avoid signal conflicts, the microprocessor, MCU, must only drive SDA and SCL low, meaning open-drain output. Open-drain mode is mainly used to protect devices and prevent interference.
- Prevent interference: multiple devices share the same data line, SDA, and the same clock line, SCL. If push-pull output mode is used, the outputs of multiple devices will overlap on the data line, causing signal interference. In severe cases, devices may be damaged or communication errors may occur. With open-drain output mode, each device can only pull the data line low and will not interfere with others, improving bus reliability and interference resistance.
- Prevent short circuits: in open-drain output mode, because the device output can only pull the data line low, no short circuit occurs even if two or more devices output at the same time. In push-pull output mode, if two or more devices output at the same time, a short circuit may form. For example, the master outputs high level while the slave outputs low level.
Because open-drain mode is used, an external pull-up resistor, such as 10k, must be connected to pull the signal to high level. Therefore, SDA, data line, and SCL, clock line, in the I2C bus are usually connected to pull-up resistors to ensure stable logic high levels. The pull-up resistance is usually between 2.2k ohms and 10k ohms, depending on bus capacitance load and communication distance. The maximum cable length and transmission capacity of the I2C bus are limited. In standard mode, the maximum cable length is about 1 meter, while in fast mode, the maximum cable length is about 0.3 meters. In addition, bus capacitance on the cable affects the transfer rate.
11.3 I2C Data Transfer
The I2C protocol uses bus arbitration for data transfer. It has only two communication lines, so data transfer is based on the clock signal. The clock is generated by the master and controls the data transfer rate. Data is sent and received by the master, but the exchange is implemented through acknowledgments from the slave. The following are several important timings of the IIC bus: Start signal: when SCL is high, SDA changes from high to low, indicating the start of one communication.
Stop signal: when SCL is high, SDA changes from low to high, indicating the end of this communication. After sending a stop signal, the master cannot send any more data to the slave unless it sends another start signal.
Data transfer: the master and slave transfer data, which can be one or more bytes. Both sending and receiving are based on address selection.
// Send one byte
void IIC_Send_Byte(unsigned char dat)
{
int i = 0;
SDA_OUT();
SCL(0);
for( i = 0; i < 8; i++ )
{
SDA( (dat & 0x80) >> 7 );
delay_us(1);
SCL(1);
delay_us(5);
SCL(0);
delay_us(5);
dat<<=1;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
I2C also provides an acknowledgment mechanism called ACK/NACK, acknowledge/not acknowledge. If a device receives data, it sends an acknowledge signal by pulling SDA low to notify the sender that the data has been received. Conversely, if the data is damaged or not received, the receiving device sends a not-acknowledge signal by keeping SDA high.
In the IIC bus, the clock line is controlled by the master. Each data bit is updated on a clock edge, and the maximum transfer rate depends on the slowest device on the bus. In general, IIC bus communication is relatively slow, usually in the range of several hundred kbps. If a higher transfer rate is required, other communication protocols such as SPI or CAN can be used.
11.4 I2C Communication Flow
I2C communication follows these steps:
- The master sends a start signal to the bus.
- The master sends the target device address and read/write bit, R/W, onto the bus.
- After receiving the address, the device sends an acknowledge signal. After the master receives the acknowledge signal, it sends data or continues sending the address.
- After receiving data, the device sends an acknowledge signal. After the master receives the acknowledge signal, it can continue sending data or stop communication.
- The master sends a stop signal to the bus.
11.5 Basic IIC Parameters
Rate: the I2C bus has standard mode, 100 kbit/s, and fast mode, 400 kbit/s. Faster extended modes and high-speed modes are also available.
Device address: each device has a unique 7-bit or 10-bit address, and address selection determines which device to communicate with.
Bus state: the I2C bus has five states: idle state, start signal, stop signal, response signal, and data transfer.
Data format: the I2C bus has two data formats, standard format and fast format. The standard format is an 8-bit data byte plus a 1-bit ACK/NACK bit. The fast format allows two bytes to be transferred at the same time.
Because the SCL and SDA lines are bidirectional, they may also have level errors caused by external factors such as line capacitance, which can cause communication errors. Therefore, in the IIC bus, pull-up resistors are usually used to keep the signal lines at high level in the idle state.
11.6 Software I2C and Hardware I2C
The I2C protocol can be implemented in software or hardware. The difference between the two methods lies in the implementation approach and required hardware resources.
11.6.1 Software I2C
Software I2C means implementing the I2C communication protocol by writing code in the program. It uses general-purpose input/output, GPIO, pins to simulate the I2C data line, SDA, and clock line, SCL, and transfers data and generates timing signals by controlling pin level changes in software. Compared with hardware I2C, the advantage of software I2C is that it does not require specific hardware support and can be implemented on any microcontroller that supports GPIO. It uses the microcontroller's general IO pins to implement the I2C communication protocol.
Software I2C simulates I2C master and slave devices through programming. It reads and writes GPIO pin states bit by bit and performs corresponding operations according to I2C protocol timing requirements, implementing data transfer and communication. Software I2C is flexible and can be customized and extended according to application requirements. It can handle multiple slave devices and supports multi-master environments. Therefore, software I2C is widely used in resource-limited MCU systems, especially applications that need to communicate with multiple external devices.
Although software I2C performance is lower than hardware I2C, it is an economical and practical solution for low-speed communication and simple communication requirements.
11.6.2 Hardware I2C
Hardware I2C means using a dedicated hardware module to process the I2C communication protocol. Most modern microcontrollers and some external devices integrate hardware I2C modules. These hardware modules handle I2C communication details, including generating correct timing signals, automatically handling signal conflicts, data transfer, error detection, and so on. Hardware pins can be connected directly without writing timing code.
Using hardware I2C is usually relatively simple. Developers do not need to write complex code to handle communication protocol details. The hardware module can connect directly to external devices and transmit data and clock signals through dedicated pins, enabling efficient and reliable communication.
When choosing software I2C or hardware I2C, consider application requirements and hardware resources. Software I2C is suitable for resource-limited systems and can be implemented on any microcontroller that supports GPIO, but its performance is relatively low. Hardware I2C usually performs better, but requires hardware support and may occupy specific pin resources.
11.6.3 Advantages and Disadvantages of IIC
11.6.3.1 Advantages
Bidirectional transmission: the I2C bus supports bidirectional transmission. Data between the master and slave devices can be transmitted through the SDA line, saving bus resources.
System integration: the I2C bus can be quickly integrated into chips, reducing logic complexity in system implementation and improving design efficiency.
Multi-device sharing: the I2C bus allows multiple devices to communicate with the master controller through address transfer, so multiple devices can share the bus and interact directly.
High reliability: the I2C bus uses logical levels instead of complex electrical signaling to represent data transfer, providing higher transfer reliability.
11.6.3.2 Disadvantages
Limited bandwidth: the I2C bus transfer speed is limited to 400 kbps. Compared with SPI and CAN buses, its bandwidth is relatively low.
Strict timing requirements: I2C bus data transfer must strictly follow timing requirements. Especially during high-speed transfer, timing is easily disturbed, causing communication failure.
Limited maximum cable length: although the I2C bus can extend bus length through repeaters, due to signal interference, signal attenuation, timing requirements, and other issues, the maximum cable length is generally limited to 1 to 2 meters.
In short, the I2C bus has advantages such as bidirectional transmission, system integration, and multi-device sharing, but it also has disadvantages such as relatively low transfer speed, strict timing requirements, and limited maximum cable length.
11.7 ESP32S3 I2C Introduction
ESP32S3 has two hardware I2C controllers, also called ports, responsible for communication on two I2C buses. Each I2C controller can operate as a master or slave. The ESP32 I2C interface can be configured as master mode or slave mode, and simple APIs can control devices on the I2C bus.
11.8 I2C-Related Functions
In the Arduino environment, using I2C requires including the built-in Wire.h library. Wire inherits from the Stream class and has all functions provided by Stream.
#include "Wire.h"Initialize I2C There are many ways to initialize I2C.
Initialize I2C with default configuration. Pin 21 is the default SDA, and pin 22 is the default SCL. However, these cannot be used on the ESP32S3R8N8 development board.
cWire.begin();1Initialize I2C with specified communication pins:
cwire.begin(int sda_pin, int scl_pin);1Initialize I2C with specified communication pins, device address, and I2C rate:
cwire.begin(uint8_t slaveAddrint sda_pin, int scl_pin);1There are many other methods. For details, check the source code of
wire.candwire.hin Arduino.
- Request data from a slave
void requestFrom(uint16_t address, uint8_t size, bool sendStop)Parameters:
address: slave address
size: number of bytes to request
sendStop: whether to send a stop signal. If true, release the IIC bus. If false, send a repeated start signal and keep the IIC bus connection.
After the request is complete, the master can use functions such as Wire.available() and Wire.read() to wait for and obtain the slave response.
Example:
Wire.requestFrom(adress,10,true);- Start transmission
void beginTransmission(int address)Parameter:
address: slave address
After this, the master can use Wire.write(); to write data and Wire.endTransmission(); to end the transmission.
Example:
Wire.beginTransmission(120);- End transmission
Wire.endTransmission();Ends data transmission but does not release IIC occupation.
endTransmission(false)
Return value: uint8_t
Meaning of return values:
| Value | Meaning |
|---|---|
| 0 | Success |
| 1 | Data too long, send buffer exceeded |
| 2 | NACK received while sending address |
| 3 | NACK received while sending data |
| 4 | Other error |
Example:
Wire.endTransmission(false);- Write data
Wire.write();When acting as master: the master adds the data to be sent to the send queue.
When acting as slave: the slave sends data to the master.
Parameters:
Wire.write(value);sends a single byteWire.write(string);sends a sequence of bytesWire.write(data,length);sends data as bytes with a specified length Return value: byte type, number of bytes written
- Check whether the receive data register has data
Wire.available();Function: returns the number of received bytes.
Return value: byte type, number of readable bytes.
- Read 1 byte of data
Wire.read()When acting as master: after the master uses requestFrom(), it uses this function to obtain data.
When acting as slave: the slave reads data sent by the master. Return value: byte data read.
- Read multiple bytes of data
size_t readBytes(char *buffer, size_t length)Parameters: buffer: receive buffer, a pointer of type charlength: data length Return value: data length
- Read until a specified character is encountered
size_t readBytesUntil(char terminator, char *buffer, size_t length)Parameters: terminator: terminating character of type charbuffer: receive buffer, a pointer of type charlength: data length Return value: data length
- Check whether the current IIC bus is busy
Wire.busy();Returns a Boolean value.
11.9 I2C Verification
11.9.1 Case 1: Display on a 0.96-inch IIC Monochrome Screen
In Arduino Library Manager, download the ESP32 OLED library:
ESP8266 and ESP32 OLED driver for SSD1306 displays/* Install the ESP32 OLED library. Search in Library Manager -> ESP8266 and ESP32 OLED driver for SSD1306 displays */
#include <Wire.h>
#include "SSD1306Wire.h"// Import the 0.96-inch screen display library
// Use the Wire library to initialize the OLED display
SSD1306Wire display(0x3c, 9, 10); // The three parameters are device address, SDA pin, and SCL pin
// Draw lines
void drawLines()
{
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(0, 0, i, display.getHeight() - 1);
display.display();
delay(10);
}
for (int16_t i = 0; i < display.getHeight(); i += 4)
{
display.drawLine(0, 0, display.getWidth() - 1, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(0, display.getHeight() - 1, i, 0);
display.display();
delay(10);
}
for (int16_t i = display.getHeight() - 1; i >= 0; i -= 4)
{
display.drawLine(0, display.getHeight() - 1, display.getWidth() - 1, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = display.getWidth() - 1; i >= 0; i -= 4)
{
display.drawLine(display.getWidth() - 1, display.getHeight() - 1, i, 0);
display.display();
delay(10);
}
for (int16_t i = display.getHeight() - 1; i >= 0; i -= 4)
{
display.drawLine(display.getWidth() - 1, display.getHeight() - 1, 0, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = 0; i < display.getHeight(); i += 4)
{
display.drawLine(display.getWidth() - 1, 0, 0, i);
display.display();
delay(10);
}
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(display.getWidth() - 1, 0, i, display.getHeight() - 1);
display.display();
delay(10);
}
delay(250);
}
// Draw rectangles
void drawRect(void)
{
for (int16_t i = 0; i < display.getHeight() / 2; i += 2)
{
display.drawRect(i, i, display.getWidth() - 2 * i, display.getHeight() - 2 * i);
display.display();
delay(10);
}
}
// Draw filled rectangles
void fillRect(void)
{
uint8_t color = 1;
for (int16_t i = 0; i < display.getHeight() / 2; i += 3)
{
display.setColor((color % 2 == 0) ? BLACK : WHITE); // alternate colors
display.fillRect(i, i, display.getWidth() - i * 2, display.getHeight() - i * 2);
display.display();
delay(10);
color++;
}
// Reset back to WHITE
display.setColor(WHITE);
}
// Draw circles
void drawCircle(void)
{
for (int16_t i = 0; i < display.getHeight(); i += 2)
{
display.drawCircle(display.getWidth() / 2, display.getHeight() / 2, i);
display.display();
delay(10);
}
delay(1000);
display.clear();
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000001);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000011);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000111);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00001111);
display.display();
}
void printBuffer(void)
{
// Initialize the log buffer
// Allocate memory to store 8 lines of text, 30 characters per line.
display.setLogBuffer(5, 30);
// Test content
const char* test[] =
{
"Hello",
"World" ,
"----",
"Show off",
"how",
"the log buffer",
"is",
"working.",
"Even",
"scrolling is",
"working"
};
for (uint8_t i = 0; i < 11; i++)
{
// Clear screen
display.clear();
// Print to screen and add a newline
display.println(test[i]);
// Draw it to the internal screen buffer
display.drawLogBuffer(0, 0);
// Display it on the screen
display.display();
delay(500);
}
}
void setup()
{
// Initialize screen
display.init();
// Set contrast
display.setContrast(255);
// Line drawing test
drawLines();
delay(1000);
// Clear screen
display.clear();
// Rectangle drawing test
drawRect();
delay(1000);
// Clear screen
display.clear();
// Filled rectangle drawing test
fillRect();
delay(1000);
// Clear screen
display.clear();
// Circle drawing test
drawCircle();
delay(1000);
display.clear();
// Text display test
printBuffer();
delay(1000);
display.clear();
}
void loop() { }2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
11.9.2 Case 2: Read Temperature Data from MLX90615
#include <Wire.h>
void setup()
{
// Initialize serial port 0 with a baud rate of 115200
Serial.begin(115200);
// Set I2C communication lines, SDA=5, SCL=6
Wire.begin(5,6);
}
void loop()
{
unsigned char buff[3]={0}; // Store temperature low byte, high byte, and checksum
uint16_t temp = 0; // Variable for merged high and low bytes
float T=0.0; // Variable for converted actual temperature
Wire.beginTransmission(0x5A); // Send the I2C start signal. Device address is 0x5A.
Wire.write(0X07); // Write the register address to operate
Wire.endTransmission(false); // Stop, then send a repeated start signal
// Read 3 bytes from the device address 0x5A, then send a stop signal after reading
Wire.requestFrom((uint16_t)0x5A, (uint8_t)3, (bool)true);
buff[0] = Wire.read();// Read the first byte, the low 8 bits of temperature data
buff[1] = Wire.read();// Read the second byte, the high 8 bits of temperature data
buff[2] = Wire.read();// Read the third byte, the checksum
// Merge high and low bytes
temp = (uint16_t)(buff[1]<<8) | buff[0];
// Convert to actual temperature
T = (temp*0.02)-273.15;
// Delay for 1 second
delay(1000);
// Output the collected temperature
Serial.printf("temp = %.2f C\r\n",T);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36