Sau khi đã làm quen với UART và SPI ở các bài trước, hôm nay chúng ta sẽ đi sâu vào I2C – một giao thức giao tiếp nối tiếp cực kỳ thông minh và phổ biến trong giới nhúng. Bên cạnh đó, chúng ta sẽ quay trở lại thực hành với UART ở cấp độ nâng cao: sử dụng Ngắt (Interrupt) kết hợp với Timer để xây dựng một hệ thống truyền nhận dữ liệu chuyên nghiệp.
🎯 Mục tiêu học tập
- Hiểu rõ nguyên lý hoạt động của bus I2C và cơ chế địa chỉ hóa thiết bị.
- Nắm vững cấu trúc phần cứng Open-Drain và vai trò của điện trở kéo lên (Pull-up).
- Phân tích chi tiết khung truyền I2C: Start, Stop, ACK/NACK và địa chỉ 7-bit.
- Thành thạo cấu hình UART Interrupt trên STM32 bằng CubeMX.
- Thực hành truyền dữ liệu theo chu kỳ và nhận dữ liệu phản hồi (Echo) bằng Ngắt.
1. Tổng quan về giao tiếp I2C (Inter-Integrated Circuit)
a) Khái niệm và Lịch sử
I2C (phát âm là I-squared-C hoặc IIC) được phát triển bởi Philips Semiconductors (nay là NXP) vào những năm 1980. Đây là chuẩn giao tiếp đồng bộ, nối tiếp, cho phép kết nối nhiều thiết bị Master và Slave trên cùng một đường truyền chỉ với 2 dây tín hiệu.
I2C là lựa chọn hàng đầu cho các ngoại vi không yêu cầu tốc độ quá cao nhưng ưu tiên sự đơn giản và tiết kiệm chân linh kiện như: Cảm biến nhiệt độ, EEPROM, đồng hồ thời gian thực (RTC), màn hình OLED…
b) Cấu tạo phần cứng: SCL và SDA
Hệ thống I2C chỉ sử dụng đúng 2 đường bus:
- SCL (Serial Clock Line): Đường xung nhịp đồng bộ do thiết bị Master phát đi.
- SDA (Serial Data Line): Đường truyền nhận dữ liệu hai chiều.
⚠️ Lưu ý quan trọng: Cả hai đường SCL và SDA đều hoạt động ở chế độ Open-Drain. Điều này có nghĩa là các thiết bị chỉ có thể kéo bus xuống mức THẤP (0V). Để có mức CAO (3.3V/5V), chúng ta bắt buộc phải sử dụng điện trở kéo lên (Pull-up) từ 1kΩ đến 4.7kΩ. Thiết kế này giúp tránh hiện tượng ngắn mạch khi nhiều thiết bị cùng giao tiếp trên bus.
c) Khung truyền dữ liệu và Địa chỉ hóa
Trong một mạng I2C, mỗi thiết bị Slave được định danh bằng một địa chỉ 7-bit duy nhất. Một khung truyền I2C điển hình bao gồm:
- Tín hiệu START: Master kéo SDA xuống thấp trước SCL.
- Địa chỉ 7-bit: Master gửi địa chỉ của Slave muốn giao tiếp.
- Bit R/W: Bit thứ 8 xác định Master muốn Đọc (1) hay Ghi (0) dữ liệu.
- Bit ACK/NACK: Slave phản hồi bằng cách kéo SDA xuống thấp nếu đúng địa chỉ.
- Dữ liệu (8-bit): Các byte dữ liệu được truyền đi, mỗi byte kèm theo 1 bit ACK xác nhận.
- Tín hiệu STOP: Master kéo SDA lên cao sau SCL để kết thúc phiên giao tiếp.
2. Thực hành cấu hình UART nâng cao trên STM32
Trong phần này, chúng ta sẽ ứng dụng Timer để gửi dữ liệu định kỳ 1 giây/lần và sử dụng Ngắt (Interrupt) để nhận dữ liệu từ máy tính gửi về vi điều khiển.
a) Các bước cấu hình trên STM32CubeMX
- Bước 1: Kích hoạt
USART1 (Mode: Asynchronous).
- Baud Rate: 115200.
- Word Length: 8 Bits.
- Stop Bits: 1.
- Bước 2: Trong tab NVIC Settings, tích chọn “USART1 global interrupt” để bật ngắt nhận.
- Bước 3: Cấu hình Timer 2 để tạo ngắt mỗi 1 giây (dùng để gửi dữ liệu định kỳ).
- Bước 4: Thiết lập ưu tiên ngắt (Preemption Priority): Ưu tiên Timer cao hơn UART (ví dụ Timer = 2, UART = 3).
b) Giải thích các hàm API quan trọng
HAL_UART_Transmit(...): Gửi dữ liệu ở chế độ Polling (chờ gửi xong mới chạy tiếp).
HAL_UART_Receive_IT(...): Kích hoạt ngắt nhận dữ liệu. CPU sẽ rảnh tay để làm việc khác cho đến khi nhận đủ số byte quy định.
HAL_UART_RxCpltCallback(...): Hàm được gọi tự động sau khi nhận đủ dữ liệu qua ngắt.
c) Ví dụ mã nguồn thực tế
Chương trình sau sẽ gửi chuỗi “Data” lên máy tính mỗi giây và sẽ phản hồi lại bất kỳ dữ liệu nào nhận được từ máy tính.
/* Khai báo biến toàn cục */
uint8_t tx_data[6] = "\nData";
uint8_t rx_buffer[6];
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
/* 1. Kích hoạt ngắt nhận 6 byte đầu tiên */
HAL_UART_Receive_IT(&huart1, rx_buffer, 6);
/* 2. Khởi động Timer tạo chu kỳ gửi */
HAL_TIM_Base_Start_IT(&htim2);
while (1) {
// Vòng lặp chính hoàn toàn rảnh rỗi cho các tác vụ khác
}
}
/* 3. Hàm xử lý khi Timer tràn (mỗi 1 giây) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
// Gửi dữ liệu định kỳ lên PC
HAL_UART_Transmit(&huart1, tx_data, 6, 500);
}
}
/* 4. Hàm xử lý khi nhận đủ 6 byte qua UART */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
// Gửi trả lại (Echo) dữ liệu vừa nhận được
HAL_UART_Transmit(&huart1, rx_buffer, 6, 200);
// Quan trọng: Kích hoạt lại ngắt nhận cho lần tiếp theo
HAL_UART_Receive_IT(&huart1, rx_buffer, 6);
}
}
3. Kết nối và Giám sát trên Máy tính
Để giao tiếp với máy tính, chúng ta sử dụng module USB to UART (PL2303/CP2102). Sơ đồ kết nối chân như sau:
| STM32 (Pin) |
Module USB-to-UART |
| PA9 (TX) |
RX |
| PA10 (RX) |
TX |
| GND |
GND |
Sử dụng phần mềm Hercules, chọn đúng cổng COM và Baudrate 115200 để quan sát dữ liệu. Bạn sẽ thấy dòng chữ “Data” xuất hiện mỗi giây, và khi bạn gửi 6 ký tự từ Hercules, vi điều khiển sẽ phản hồi lại ngay lập tức.
🚀 Bài tập thực hành
- Địa chỉ I2C: Tra cứu Datasheet của cảm biến nhiệt độ LM75 hoặc module DS3231. Cho biết địa chỉ I2C mặc định của chúng là gì?
- Xử lý chuỗi UART: Cấu hình ngắt nhận từng ký tự (Size = 1). Nếu nhận được ký tự ‘R’, bật LED Đỏ. Nếu nhận được ký tự ‘G’, bật LED Xanh.
- Nâng cao: Kết hợp UART và Timer. Nhận một con số từ máy tính qua UART (ví dụ ‘5’), sau đó thay đổi chu kỳ gửi dữ liệu của Timer thành 5 giây.
📝 Tóm tắt: I2C là giao thức mạnh mẽ cho việc kết nối đa thiết bị với số lượng chân tối thiểu. Trong khi đó, việc làm chủ UART ở chế độ Ngắt là kỹ năng bắt buộc để xử lý các luồng dữ liệu bất đồng bộ mà không làm treo hệ thống. Hãy luôn nhớ cấu hình Pull-up cho I2C và gọi lại hàm Receive_IT sau mỗi lần Callback UART thành công.
“Kỹ sư giỏi không chỉ truyền được dữ liệu, mà còn biết cách truyền dữ liệu một cách hiệu quả và tin cậy.”
Gợi ý bài tiếp theo: Thực Hành Lập Trình SPI Với DAC MCP4922 Và I2C Với EEPROM AT24C32.