Bỏ qua, đến nội dung chính
Bài viếtFirmwareMới

STM32 Beginner - P3: UART, I2C và SPI thực hành với HAL

Tìm hiểu ba giao tiếp phổ biến trên STM32: dùng UART để debug, I2C để đọc cảm biến và SPI để kết nối màn hình hoặc bộ nhớ bằng STM32CubeIDE và HAL.

Chia sẻ

LinkedInFacebookX
STM32 Nucleo board connected to UART, I2C, and SPI test hardware on an electronics workbench

P2: GPIO, Interrupt và Timer, mình đã dùng GPIO, interrupt và timer để làm firmware phản ứng với tín hiệu bên ngoài. Nhưng khi project lớn hơn một chút, bạn sẽ cần một cách để nói chuyện với laptop, cảm biến, màn hình hoặc chip nhớ.

Đi trong series: P1: Bắt đầu với STM32 · P2: GPIO, Interrupt và Timer · P3 đang đọc

Ba giao tiếp xuất hiện nhiều nhất trong các project STM32 là UART, I2C và SPI. Chúng đều truyền dữ liệu tuần tự, nhưng cách hoạt động rất khác nhau. Chọn sai giao tiếp hoặc nối nhầm một sợi dây có thể khiến firmware “chạy đúng”, nhưng thiết bị bên ngoài không bao giờ trả lời.

Trong bài này, mình vẫn dùng NUCLEO-G071RB, STM32CubeIDE và HAL. Các ví dụ chính thức của ST cũng được tổ chức theo từng peripheral và có các kiểu truyền polling, interrupt và DMA trong STM32CubeG0.

Nhìn nhanh: chọn giao tiếp nào?

Giao tiếpĐường tín hiệu cơ bảnĐiểm mạnhĐiểm cần đổi lại
UARTTX, RX, GNDDễ debug, kết nối point-to-pointKhông có clock chung, cần thống nhất baud và format
I2CSDA, SCL, GNDNhiều thiết bị dùng chung hai dâyCần địa chỉ, pull-up và tốc độ thường thấp hơn SPI
SPISCK, MOSI, MISO, CS, GNDNhanh, full-duplex, hợp với display/flashNhiều dây hơn; mỗi thiết bị thường cần một CS

Một quy tắc thực tế của mình là: UART để nhìn, I2C để hỏi, SPI để đẩy dữ liệu nhanh. Đây không phải là luật bắt buộc, nhưng giúp bạn chọn điểm bắt đầu khá nhanh.

Chuẩn bị trong CubeMX

Trước khi viết code, tạo project cho đúng MCU trên board. Sau đó:

  1. Chọn peripheral cần dùng trong mục Connectivity.
  2. Kiểm tra pin mapping và các alternate functions.
  3. Chọn mode, tốc độ, data size và interrupt/DMA nếu cần.
  4. Đặt tên handle dễ đọc như huart2, hi2c1, hspi1.
  5. Generate code và chỉ viết phần riêng trong vùng USER CODE.

Tên USARTUART đôi khi được dùng lẫn lộn trong tutorial. Hãy nhìn đúng peripheral mà MCU hỗ trợ. Với UART không có clock ngoài, bạn thường chỉ cấu hình TX/RX; USART có thể có thêm synchronous clock và các mode khác.

UART: đường serial đầu tiên để debug

Board STM32 Nucleo nối USB với laptop và mạch UART để kiểm tra log serial.

UART là công cụ debug đơn giản nhất: trước khi nghi ngờ sensor, hãy in ra trạng thái mà firmware đang nhìn thấy.

UART không dùng clock chung giữa hai thiết bị. Bên gửi và bên nhận phải thống nhất baud rate, số bit dữ liệu, parity và stop bit. Cấu hình hay gặp nhất là:

115200 baud, 8 data bits, no parity, 1 stop bit

Bạn sẽ thấy cấu hình này viết ngắn là 115200 8N1.

Nối UART đúng cách

UART tối thiểu cần:

STM32 TX ---- thiết bị RX
STM32 RX ---- thiết bị TX
STM32 GND --- thiết bị GND

TX nối chéo sang RX, không nối TX với TX. Hai thiết bị cũng cần chung GND để có cùng mốc điện áp.

UART trên STM32 dùng logic theo điện áp của MCU, thường là 3,3 V. Đừng nối thẳng một cổng RS-232 thật vào GPIO; RS-232 dùng mức điện áp và cực tính khác, cần transceiver phù hợp. USB-UART adapter cũng phải được kiểm tra mức logic trước khi cắm.

Truyền dữ liệu bằng polling

Giả sử CubeMX đã sinh ra huart2, đoạn debug nhỏ nhất là:

const char message[] = "hello from STM32\\r\\n";

HAL_UART_Transmit(
    &huart2,
    (uint8_t *)message,
    sizeof(message) - 1,
    100
);

HAL_UART_Transmit() là kiểu polling/blocking: CPU chờ cho đến khi truyền xong hoặc timeout. Với log thưa và chuỗi ngắn, đây là cách dễ hiểu nhất.

Khi muốn nhận một byte:

uint8_t rx_byte;

if (HAL_UART_Receive(&huart2, &rx_byte, 1, 100) == HAL_OK)
{
    HAL_UART_Transmit(&huart2, &rx_byte, 1, 100);
}

Đây là ví dụ echo. Nó hữu ích để kiểm tra baud, dây TX/RX và terminal trước khi thêm protocol.

Khi nào dùng interrupt hoặc DMA?

Polling dễ bắt đầu nhưng sẽ giữ CPU trong lúc chờ. Với UART nhận dữ liệu liên tục, bạn có thể chuyển sang:

  • HAL_UART_Transmit_IT() / HAL_UART_Receive_IT() khi buffer nhỏ và muốn xử lý theo interrupt.
  • HAL_UART_Transmit_DMA() / HAL_UART_Receive_DMA() khi có stream dài hoặc tốc độ cao hơn.

Đừng bắt đầu bằng DMA chỉ vì nó “chuyên nghiệp” hơn. Hãy làm cho polling chạy đúng, xác định frame và timeout, sau đó mới tối ưu. UART DMA mà không có cách phát hiện end-of-frame thường chỉ chuyển vấn đề sang một nơi khác.

I2C: nhiều thiết bị trên cùng một bus

Board STM32 Nucleo nối với cảm biến nhiệt độ và màn hình OLED qua các dây SDA, SCL, nguồn và GND.

I2C chỉ dùng hai đường tín hiệu chung cho nhiều thiết bị, nhưng mỗi thiết bị phải có địa chỉ không trùng nhau.

I2C dùng hai đường tín hiệu:

  • SDA: dữ liệu.
  • SCL: clock.

Các thiết bị cùng nối vào bus và thiết bị chủ, thường là STM32, tạo clock. Mỗi thiết bị có địa chỉ để master chọn đúng nơi cần đọc hoặc ghi.

Pull-up là bắt buộc

SDA và SCL thường là đường open-drain/open-collector. Thiết bị kéo đường xuống LOW; điện trở pull-up kéo đường lên HIGH. Vì vậy, bus cần pull-up lên đúng rail logic, thường là 3,3 V.

Nhiều breakout sensor đã có sẵn pull-up. Nếu bạn nối ba module đều có điện trở 4,7 kΩ, tổng trở kéo lên có thể thấp hơn dự kiến và làm tăng dòng khi đường bus ở LOW. Không phải lúc nào thêm pull-up cũng tốt hơn.

Địa chỉ I2C là 7-bit hay 8-bit?

Datasheet thường ghi địa chỉ I2C dạng 7-bit, ví dụ 0x76 hoặc 0x77. Một số thư viện hoặc tài liệu cũ lại hiển thị byte đã shift trái và thêm bit R/W. Đây là lý do cùng một sensor đôi khi xuất hiện với hai địa chỉ khác nhau.

Với HAL, hãy kiểm tra tài liệu API và cách thư viện đang mong đợi địa chỉ. Nhiều hàm HAL nhận địa chỉ đã shift trái một bit:

uint16_t device_address = 0x76 << 1;

Đừng tự shift hai lần. Khi thiết bị không trả lời, đây là một trong những điểm đầu tiên mình kiểm tra.

Kiểm tra thiết bị bằng HAL_I2C_IsDeviceReady

if (HAL_I2C_IsDeviceReady(&hi2c1, device_address, 3, 100) == HAL_OK)
{
    // Thiết bị đã ACK địa chỉ
}
else
{
    // Kiểm tra nguồn, GND, SDA, SCL, pull-up và địa chỉ
}

Hàm này chỉ cho biết thiết bị phản hồi ở địa chỉ đó. Nó chưa chứng minh rằng timing, register map hay dữ liệu sensor đã đúng.

Đọc và ghi register

Một sensor I2C thường có register address. Ví dụ tổng quát:

uint8_t config = 0x01;
uint8_t data[2];

HAL_I2C_Mem_Write(&hi2c1, device_address, 0x10,
                  I2C_MEMADD_SIZE_8BIT, &config, 1, 100);

HAL_I2C_Mem_Read(&hi2c1, device_address, 0x20,
                 I2C_MEMADD_SIZE_8BIT, data, sizeof(data), 100);

Các giá trị 0x100x20 ở đây chỉ là ví dụ. Bạn phải lấy register và format dữ liệu từ datasheet của đúng sensor. Đọc được hai byte không có nghĩa là bạn đã đọc đúng nhiệt độ; còn phải xử lý endian, scale, signed bit và trạng thái data-ready.

SPI: nhanh hơn, nhiều dây hơn

Board STM32 Nucleo nối màn hình màu SPI và logic analyzer trên bàn thử nghiệm điện tử.

SPI thường nhanh hơn I2C, đổi lại cần thêm chip-select và phải kiểm tra đúng mode, thứ tự bit và mức logic.

SPI thường dùng:

  • SCK: clock do master tạo.
  • MOSI: master out, slave in.
  • MISO: master in, slave out.
  • CS hoặc NSS: chọn slave.
  • GND: mốc chung.

Mỗi thiết bị SPI thường có một CS riêng. SCK, MOSIMISO có thể dùng chung nếu các thiết bị không được chọn đồng thời và thiết kế bus phù hợp.

Cấu hình SPI cơ bản

Trong CubeMX, chọn master, 2-line full-duplex, data size 8 bit, MSB first, rồi chọn CPOL/CPHA theo datasheet của slave. Bốn tổ hợp clock mode là:

ModeCPOLCPHA
000
101
210
311

Không nên đoán mode từ tên “SPI display”. Hai module cùng dùng SPI vẫn có thể yêu cầu mode khác nhau.

CS thường do firmware điều khiển

Với nhiều thiết bị, CS là một GPIO bình thường:

HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);

HAL_SPI_Transmit(&hspi1, tx_buffer, tx_length, 100);

HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);

Giữ CS LOW trong toàn bộ transaction cần thiết. Một số thiết bị yêu cầu CS HIGH giữa command và data, một số lại không; hãy đọc timing diagram.

SPI có thể truyền và nhận cùng lúc:

HAL_SPI_TransmitReceive(&hspi1, tx_buffer, rx_buffer, length, 100);

Với display chỉ nhận dữ liệu, MISO có thể không cần nối. Với flash hoặc sensor cần đọc, MISO là bắt buộc.

Polling, interrupt hay DMA?

STM32CubeG0 có example cho nhiều kiểu truyền. Mình thường chọn theo bảng này:

Kiểu truyềnDùng khiĐiểm cần chú ý
PollingSetup ban đầu, transaction ngắnCPU bị chặn trong lúc chờ
InterruptMuốn main loop tiếp tục chạy, buffer vừaCần state machine và callback rõ
DMABuffer dài, tần suất cao, display/audioCần quản lý buffer, cache và thời điểm hoàn tất

Đừng trộn nhiều transaction trên cùng peripheral khi transaction trước chưa hoàn thành. Với interrupt/DMA, kiểm tra trạng thái HAL_BUSY, callback complete và callback error trước khi bắt đầu lần truyền mới.

Một quy trình debug giao tiếp mình hay dùng

Khi sensor hoặc display không phản hồi, mình đi theo thứ tự:

  1. Kiểm tra nguồn và GND bằng multimeter.
  2. Kiểm tra mức logic: thiết bị có chịu 3,3 V không?
  3. Đọc lại pin mapping trong CubeMX và schematic.
  4. Giảm baud/tốc độ I2C hoặc SPI xuống mức thấp.
  5. Với UART, kiểm tra TX/RX chéo và cấu hình 8N1.
  6. Với I2C, kiểm tra pull-up và scan đúng địa chỉ 7 bit.
  7. Với SPI, kiểm tra CS, CPOL/CPHA, MSB/LSB và timing.
  8. Dùng logic analyzer để xem bus có thật sự có xung và ACK không.

Nếu bus chưa chạy được ở tốc độ thấp, tăng tốc chỉ làm waveform khó nhìn hơn. Hãy làm transaction nhỏ nhất: UART gửi một dòng, I2C đọc một ID register, hoặc SPI gửi một command rồi kiểm tra phản hồi.

Lỗi dễ gặp

UART im lặng

Thường là sai cổng COM, sai baud, TX/RX nối thẳng thay vì nối chéo, thiếu GND chung hoặc chọn nhầm USART/alternate function pin. Với Nucleo, hãy kiểm tra đúng cổng ST-LINK virtual COM và solder bridge theo user manual của board.

I2C luôn timeout

Kiểm tra pull-up, địa chỉ, nguồn sensor và xem SDA/SCL có bị giữ LOW không. Một module chạy ở 5 V cũng không đồng nghĩa với việc logic I2C của nó an toàn với STM32 3,3 V.

I2C có ACK nhưng dữ liệu sai

Có thể bạn đang dùng sai register, sai độ dài địa chỉ register, sai endian hoặc đọc trước khi sensor hoàn thành conversion. ACK chỉ xác nhận tầng bus, không xác nhận tầng protocol của sensor.

SPI có clock nhưng màn hình không hiển thị

Kiểm tra CS, chân reset/DC của display, SPI mode và thứ tự command. Một màn hình SPI thường có thêm DCRESET; hai chân đó không tự biến mất chỉ vì bus đã có SCK/MOSI.

Giao tiếp chạy một lúc rồi hỏng

Kiểm tra timeout, xử lý lỗi và việc bắt đầu transaction mới khi transaction cũ chưa hoàn tất. Với dây dài, giảm tốc độ, nối GND tốt hơn và xem lại return path.

Kết luận

UART là lựa chọn tốt nhất để tạo “đôi mắt” cho firmware. I2C giúp nhiều sensor dùng chung một bus nhỏ gọn. SPI cần nhiều dây hơn nhưng phù hợp với display, flash và các transaction cần throughput cao.

P3 không nhằm biến bạn thành chuyên gia protocol ngay lập tức. Mục tiêu thực tế hơn là bạn biết bắt đầu từ transaction nhỏ, đọc đúng datasheet, kiểm tra tầng điện trước tầng protocol và chỉ chuyển sang interrupt/DMA khi polling đã được xác nhận.

P4: ADC, PWM và DMA, mình sẽ ghép UART, I2C và timer thành một data logger nhỏ: đọc cảm biến theo chu kỳ, in log có cấu trúc và xử lý lỗi khi thiết bị không trả lời.

Tham khảo và mã nguồn mở

Chia sẻ

LinkedInFacebookX

Tiếp tục khám phá

Đọc tiếp

Bài viết liên quan

Xem thêm trong Bài viết

Nastrotek sử dụng cookie để phân tích truy cập và cá nhân hóa quảng cáo, giúp hiểu cách website được sử dụng. Bạn có thể chấp nhận hoặc từ chối các cookie không thiết yếu. Chính sách bảo mật