I2C (Inter-Integrated Circuit) là một trong những chuẩn giao tiếp phổ biến nhất khi làm việc với I2C trong STM32 và các module cảm biến. I2C chỉ dùng hai dây để truyền dữ liệu giữa nhiều thiết bị, giúp tiết kiệm chân vi điều khiển mà vẫn kết nối được hàng chục IC trên cùng một bus. Bài viết này trình bày nguyên lý hoạt động của I2C và hai cách triển khai trên STM32F1: I2C Software (tự “đập bit” bằng GPIO) và I2C Hardware (dùng ngoại vi I2C tích hợp).
Nguyên lý giao thức I2C trong STM32
I2C chỉ sử dụng hai dây để truyền dữ liệu giữa các thiết bị:
- SDA (Serial Data) – đường truyền để master và slave gửi và nhận dữ liệu.
- SCL (Serial Clock) – đường mang tín hiệu xung nhịp.

Cả hai đường SDA và SCL đều hoạt động ở chế độ Open-Drain kèm điện trở kéo lên (pull-up). Nhờ vậy, nhiều thiết bị có thể cùng nối vào bus mà không xung đột mức logic: khi rảnh, cả hai đường được kéo lên mức cao; thiết bị nào muốn phát mức 0 chỉ việc kéo đường xuống GND. Các chân có thể vừa là ngõ vào, vừa là ngõ ra tùy vào vai trò Master/Slave khi truyền.

Một khung truyền (frame) I2C bắt đầu bằng tín hiệu Start: SDA được kéo xuống 0 trước SCL một khoảng delay nhỏ. Sau đó là 7 (hoặc 10) bit địa chỉ để chọn slave, 1 bit R/W xác định hướng đọc/ghi, rồi các byte dữ liệu 8 bit. Sau mỗi byte, bên nhận trả về 1 bit ACK/NACK ở xung clock thứ 9. Khung kết thúc bằng tín hiệu Stop: SCL được kéo lên 1 trước SDA một khoảng delay nhỏ.

I2C Software (bit-banding bằng GPIO)
I2C Software là cách tự tạo tín hiệu I2C bằng cách điều khiển trực tiếp hai chân GPIO. Ưu điểm là có thể dùng bất kỳ cặp chân nào, dễ hiểu bản chất; nhược điểm là tốn CPU và tốc độ phụ thuộc hàm delay.
Cấu hình GPIO
Hai chân SDA/SCL được cấu hình ở chế độ Output Open-Drain (GPIO_Mode_Out_OD) để phù hợp với bản chất bus I2C:
#define I2C_SCL GPIO_Pin_6
#define I2C_SDA GPIO_Pin_7
#define I2C_GPIO GPIOB
void RCC_Config(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
}
void GPIO_Config(void){
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; /* Open-Drain */
GPIO_InitStructure.GPIO_Pin = I2C_SDA | I2C_SCL;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_GPIO, &GPIO_InitStructure);
}
Để đơn giản hóa việc điều khiển từng chân, ta định nghĩa sẵn các macro đọc/ghi SDA, SCL:
#define WRITE_SDA_0 GPIO_ResetBits(I2C_GPIO, I2C_SDA)
#define WRITE_SDA_1 GPIO_SetBits(I2C_GPIO, I2C_SDA)
#define WRITE_SCL_0 GPIO_ResetBits(I2C_GPIO, I2C_SCL)
#define WRITE_SCL_1 GPIO_SetBits(I2C_GPIO, I2C_SCL)
#define READ_SDA_VAL GPIO_ReadInputDataBit(I2C_GPIO, I2C_SDA)
Tín hiệu Start và Stop
Ở trạng thái không truyền, cả hai đường SDA và SCL đều ở mức cao. Bản tin bắt đầu bằng Start (SDA xuống 0 trước SCL) và kết thúc bằng Stop (SCL lên 1 trước SDA):
void I2C_Start(void){
WRITE_SCL_1;
delay_us(3);
WRITE_SDA_1;
delay_us(3);
WRITE_SDA_0; /* SDA về 0 trước SCL */
delay_us(3);
WRITE_SCL_0;
delay_us(3);
}
void I2C_Stop(void){
WRITE_SDA_0;
delay_us(3);
WRITE_SCL_1; /* SCL lên 1 trước SDA */
delay_us(3);
WRITE_SDA_1;
delay_us(3);
}
Hàm truyền (Write)
Hàm truyền sẽ gửi lần lượt 8 bit trong byte dữ liệu, mỗi bit kèm một xung clock, sau đó chờ nhận lại tín hiệu ACK ở xung thứ 9:
- Truyền 1 bit (đưa bit cao nhất ra SDA).
- Tạo clock trên SCL.
- Dịch trái dữ liệu 1 bit.
- Chờ nhận lại ACK ở xung thứ 9.

status I2C_Write(uint8_t u8Data){
uint8_t i;
status stRet;
for (i = 0; i < 8; i++){ /* Gửi lần lượt 8 bit */
if (u8Data & 0x80) {
WRITE_SDA_1;
} else {
WRITE_SDA_0;
}
delay_us(3);
WRITE_SCL_1;
delay_us(5);
WRITE_SCL_0;
delay_us(2);
u8Data <<= 1; /* Dịch sang bit kế tiếp */
}
/* Xung thứ 9: đọc ACK từ slave */
WRITE_SDA_1;
delay_us(3);
WRITE_SCL_1;
delay_us(3);
if (READ_SDA_VAL) {
stRet = NOT_OK; /* SDA = 1 → NACK */
} else {
stRet = OK; /* SDA = 0 → ACK */
}
delay_us(2);
WRITE_SCL_0;
delay_us(5);
return stRet;
}
Hàm nhận (Read)
Hàm nhận sẽ đọc lần lượt 8 bit trên đường SDA, sau đó chủ động gửi lại 1 tín hiệu ACK (hoặc NACK) ở xung thứ 9:
- Kéo SDA lên 1 (nhả bus cho slave điều khiển).
- Đọc mức data trên SDA, ghi vào biến.
- Dịch 1 bit.
- Gửi lại tín hiệu ACK ở xung thứ 9.
uint8_t I2C_Read(ACK_Bit _ACK){
uint8_t i;
uint8_t u8Ret = 0x00;
WRITE_SDA_1;
delay_us(3);
for (i = 0; i < 8; ++i) {
u8Ret <<= 1;
WRITE_SCL_1;
delay_us(3);
if (READ_SDA_VAL) {
u8Ret |= 0x01;
}
delay_us(2);
WRITE_SCL_0;
delay_us(5);
}
/* Xung thứ 9: gửi ACK/NACK */
if (_ACK) {
WRITE_SDA_0; /* ACK */
} else {
WRITE_SDA_1; /* NACK */
}
delay_us(3);
WRITE_SCL_1;
delay_us(5);
WRITE_SCL_0;
delay_us(5);
return u8Ret;
}
I2C Hardware (dùng ngoại vi I2C tích hợp)
I2C Hardware sử dụng bộ ngoại vi I2C có sẵn trong STM32. Cách này chạy nhanh, ít tốn CPU và tự động xử lý clock, ACK, cờ trạng thái. Bù lại, chân sử dụng là cố định theo datasheet.
Cấu hình GPIO
Chân của từng bộ I2C được quy định sẵn trong datasheet. Với I2C1 của STM32F103, hai chân mặc định là PB6 (SCL) và PB7 (SDA):

Vì bus có trở kéo lên nên chân được cấu hình ở chế độ Alternate Function Open-Drain (GPIO_Mode_AF_OD): SCL là Output, còn SDA vừa Input vừa Output.

#define I2C_SCL GPIO_Pin_6
#define I2C_SDA GPIO_Pin_7
#define I2C1_GPIO GPIOB
void GPIO_Config(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; /* Alternate Function Open-Drain */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
Cấu hình I2C
Tương tự các ngoại vi khác, các tham số I2C được khai báo trong struct I2C_InitTypeDef:
| Tham số | Ý nghĩa |
|---|---|
I2C_Mode | Chế độ hoạt động: I2C_Mode_I2C (Fast Mode), hoặc I2C_Mode_SMBusDevice/I2C_Mode_SMBusHost (Slow Mode). |
I2C_ClockSpeed | Tốc độ clock: tối đa 100 kHz với SM và 400 kHz với FM. |
I2C_DutyCycle | Chu kỳ nhiệm vụ xung: I2C_DutyCycle_2 (thấp/cao = 2) hoặc I2C_DutyCycle_16_9. |
I2C_OwnAddress1 | Địa chỉ của thiết bị khi làm slave. |
I2C_Ack | Có sử dụng ACK hay không. |
I2C_AcknowledgedAddress | Số bit địa chỉ: 7 hoặc 10 bit. |
Các hàm và sự kiện quan trọng
Một số hàm SPL thường dùng khi giao tiếp I2C Hardware:
I2C_Send7bitAddress(I2Cx, Address, I2C_Direction)– gửi 7 bit địa chỉ để xác định slave; hướng truyền doI2C_Directionquyết định (thêm bit R/W).I2C_SendData(I2Cx, Data)– gửi đi 8 bit data.I2C_ReceiveData(I2Cx)– trả về 8 bit data nhận được.I2C_CheckEvent(I2Cx, I2C_EVENT)– kiểm tra một sự kiện I2C tương ứng.
Các sự kiện (I2C_EVENT) hay được kiểm tra bằng I2C_CheckEvent():
| Sự kiện (I2C_EVENT) | Ý nghĩa |
|---|---|
I2C_EVENT_MASTER_MODE_SELECT | Đợi Bus I2C về chế độ rảnh (sẵn sàng). |
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED | Đợi Slave xác nhận yêu cầu ghi của Master. |
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED | Đợi Slave xác nhận yêu cầu đọc của Master. |
I2C_EVENT_MASTER_BYTE_TRANSMITTED | Đợi truyền xong 1 byte data từ Master. |
I2C_EVENT_MASTER_BYTE_RECEIVED | Đợi Master nhận đủ 1 byte data. |
Quy trình truyền – nhận
Bắt đầu truyền nhận, bộ I2C tạo tín hiệu Start, đợi Bus sẵn sàng, rồi gửi 7 bit địa chỉ để chọn slave và đợi slave xác nhận:
I2C_GenerateSTART(I2C1, ENABLE);
/* Đợi Bus sẵn sàng */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, 0x44, I2C_Direction_Transmitter);
/* Đợi slave xác nhận yêu cầu ghi */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
Sau đó gửi hoặc đọc các byte data và đợi truyền/nhận xong:
void Send_I2C_Data(uint8_t data){
I2C_SendData(I2C1, data);
/* Đợi cờ báo đã truyền xong data */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
uint8_t Read_I2C_Data(void){
uint8_t data = I2C_ReceiveData(I2C1);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
return data;
}
Cuối cùng, kết thúc phiên giao tiếp bằng tín hiệu Stop: I2C_GenerateSTOP(I2C1, ENABLE);
So sánh I2C Software và I2C Hardware
| Tiêu chí | I2C Software | I2C Hardware |
|---|---|---|
| Cách tạo tín hiệu | Điều khiển GPIO thủ công (bit-banging) | Ngoại vi I2C tự động |
| Chân sử dụng | Bất kỳ cặp chân GPIO nào | Cố định theo datasheet (PB6/PB7…) |
| Tốc độ | Chậm, phụ thuộc hàm delay | Nhanh (100 kHz SM, 400 kHz FM) |
| Tải CPU | Cao (CPU tạo từng xung) | Thấp (phần cứng xử lý) |
| Độ linh hoạt / dễ hiểu | Cao, thấy rõ bản chất giao thức | Phụ thuộc thanh ghi và cờ trạng thái |
Kết luận
Qua bài viết, bạn đã nắm được nguyên lý I2C trong STM32: giao tiếp 2 dây SDA/SCL ở chế độ Open-Drain có điện trở kéo lên, khung truyền gồm Start – Address – R/W – ACK – Data – Stop. Với I2C Software, ta tự tạo tín hiệu bằng GPIO và các hàm Start/Stop/Write/Read, giúp hiểu sâu bản chất giao thức. Với I2C Hardware, ta tận dụng ngoại vi I2C cùng I2C_InitTypeDef và các sự kiện I2C_CheckEvent() để truyền nhận nhanh và nhẹ CPU.
Trong thực tế automotive embedded, I2C thường dùng để đọc cảm biến (nhiệt độ, gia tốc, EEPROM…). Hiểu cả hai cách triển khai giúp bạn linh hoạt lựa chọn: dùng I2C Hardware khi cần hiệu năng, hoặc I2C Software khi hết chân cứng hoặc cần chạy trên chân bất kỳ.


