Giao thức truyền thông trên STM32 (Phần 1): UART – khung dữ liệu, baudrate và nhận DMA với ngắt IDLE
UART trên STM32 từ khung dữ liệu, cách tính sai số baudrate đến nhận gói dài không biết trước bằng DMA và ngắt IDLE. Có sơ đồ, mã HAL và bảng lỗi hay gặp. Phần 1 của series giao thức truyền thông trên STM32.

Ghi chú: sơ đồ trong bài do Capitlab vẽ để minh hoạ. Số liệu trong các bảng là giá trị tính theo công thức, cột "Đo thực tế" sẽ được cập nhật khi đo trên phần cứng. Mã nguồn soạn theo tài liệu HAL và datasheet, chưa chạy thử trên phần cứng, nên hãy kiểm tra lại trước khi dùng cho sản phẩm.
Dự án nhúng nào cũng có ít nhất một cổng UART, dù chỉ để in log. Giao thức này đơn giản, nhưng các lỗi khó chịu nhất với nó (mất byte, ký tự rác, tràn bộ đệm) thường quy về hai câu hỏi: baudrate thực lệch bao nhiêu so với baudrate đặt, và làm sao biết một gói dữ liệu đã kết thúc.
Bài này trả lời hai câu hỏi đó trên STM32. Phần đầu là khung dữ liệu và cách tính sai số. Phần sau là cách nhận gói có độ dài bất kỳ bằng DMA vòng (circular) kết hợp ngắt IDLE, kèm một chi tiết hay làm người mới hiểu sai: tham số Size trong callback là vị trí ghi của DMA, không phải số byte vừa nhận.
1. Khung dữ liệu UART
UART truyền bất đồng bộ: hai bên không dùng chung clock mà thống nhất trước tốc độ và khuôn dạng. Khi rảnh, đường truyền ở mức cao. Mỗi ký tự gồm:
- Một bit start, kéo đường xuống thấp.
- Từ 5 đến 9 bit dữ liệu, bit thấp đi trước (LSB first). Hay dùng nhất là 8 bit.
- Một bit parity chẵn hoặc lẻ, có thể bỏ.
- Một hoặc hai bit stop, đưa đường về mức cao.
Hình 1. Khung UART 8N1 khi gửi ký tự "A" (0x41 = 01000001b). Bit D0 đi trước, nên chuỗi bit trên đường là 1, 0, 0, 0, 0, 0, 1, 0.
Khuôn dạng 8N1 (8 bit dữ liệu, không parity, 1 bit stop) chiếm 10 bit cho mỗi byte. Ở 115200 baud, thông lượng tối đa vì thế là 11 520 byte/s chứ không phải 14 400.
Bên nhận bắt cạnh xuống của bit start, sau đó lấy mẫu mỗi bit ở khoảng giữa chu kỳ. Nếu tốc độ hai bên lệch nhau nhiều, điểm lấy mẫu trôi dần và tới bit cuối thì rơi sang bit bên cạnh, gây lỗi khung (framing error). Với khung 10 bit, giới hạn lý thuyết của tổng độ lệch hai bên vào khoảng 5%. Trên thực tế nên giữ dưới 2% để còn dư cho nhiễu và cạnh xung chậm.
2. Baudrate và sai số trên STM32
STM32 tạo baudrate bằng cách chia clock của khối USART (fck). Ở chế độ lấy mẫu 16 lần (oversampling by 16, mặc định), thanh ghi BRR chứa hệ số chia theo đơn vị 1/16:
BRR ≈ fck / baud (làm tròn đến số nguyên), và baudrate thực = fck / BRR
Vì BRR là số nguyên, baudrate thực hiếm khi đúng bằng giá trị đặt. Bảng dưới là kết quả tính với fck = 84 MHz, oversampling 16. Đây là số tính, chưa phải số đo:
| Baudrate mong muốn | BRR | Baudrate thực | Sai số | Thời gian 1 bit | Thông lượng tối đa (8N1) |
|---|---|---|---|---|---|
| 9 600 | 8750 | 9 600 | 0,000% | 104,17 µs | 960 byte/s |
| 57 600 | 1458 | 57 613 | +0,023% | 17,36 µs | 5 760 byte/s |
| 115 200 | 729 | 115 226 | +0,023% | 8,68 µs | 11 520 byte/s |
| 230 400 | 365 | 230 137 | −0,114% | 4,34 µs | 23 040 byte/s |
| 460 800 | 182 | 461 538 | +0,160% | 2,17 µs | 46 080 byte/s |
| 921 600 | 91 | 923 077 | +0,160% | 1,09 µs | 92 160 byte/s |
| 1 843 200 | 46 | 1 826 087 | −0,928% | 0,54 µs | 184 320 byte/s |
| 3 000 000 | 28 | 3 000 000 | 0,000% | 0,33 µs | 300 000 byte/s |
Bảng cho thấy độ chính xác phụ thuộc vào việc baudrate có chia "đẹp" theo clock hay không. Ở 84 MHz, 3 Mbaud chia đúng (BRR = 28), còn 1,8432 Mbaud lệch gần 1%. Nếu được tự chọn clock hệ thống, hãy chọn sao cho các baudrate bạn cần chia tròn.
Nguồn clock còn ảnh hưởng nhiều hơn hệ số chia. Dao động RC nội (HSI) kém chính xác và trôi theo nhiệt độ hơn thạch anh ngoài (HSE), nên khi chạy tốc độ cao hoặc cần chắc chắn thì dùng HSE. Con số cụ thể nằm trong datasheet của từng dòng chip.
Với oversampling 16, tốc độ tối đa là fck / 16, tức 5,25 Mbaud ở 84 MHz.
3. Nối dây
Hình 2. Nối chéo TX với RX, và luôn chung GND. Tên chân PA2 và PA3 ví dụ cho USART2 trên nhiều board STM32F4, hãy kiểm tra chân của dòng bạn dùng.
TX bên này nối RX bên kia và ngược lại. Hai bên phải chung GND: thiếu dây này là lý do phổ biến của ký tự rác xuất hiện ngẫu nhiên.
Mức logic cũng phải khớp. STM32 chạy 3,3 V, nên khi nối với thiết bị 5 V cần xem chân RX có chịu được 5 V không (chân FT, five-volt tolerant). Cổng RS-232 trên máy tính cũ dùng khoảng ±12 V và sẽ làm hỏng chân MCU nếu nối thẳng. Trường hợp đó cần mạch chuyển mức như MAX3232, hoặc dùng chip USB-UART. Khi cần kéo dây xa trong môi trường nhiễu, chuyển sang RS-485 (phần 6).
4. Ba cách nhận dữ liệu
| Chế độ | Hàm HAL | Khi nên dùng | Nhược điểm |
|---|---|---|---|
| Polling | HAL_UART_Receive |
In log, thử nghiệm nhanh | Chặn CPU, dễ mất byte ở tốc độ cao |
| Ngắt | HAL_UART_Receive_IT |
Ít dữ liệu, độ dài biết trước | Mỗi byte một ngắt, tốn CPU |
| DMA + IDLE | HAL_UARTEx_ReceiveToIdle_DMA |
Dữ liệu liên tục, độ dài bất kỳ | Cấu hình nhiều bước hơn |
UART không có ký tự "hết gói" chuẩn. Trên STM32, cách hay dùng là ngắt IDLE line: sau khi đã nhận dữ liệu, nếu đường RX im lặng quá thời gian một ký tự thì ngoại vi báo IDLE. Ghép với DMA, cả gói được chép vào RAM mà CPU chỉ bị gọi một lần ở cuối gói.
5. Cấu hình trong STM32CubeIDE
- Mở file
.ioc, mục Connectivity, bật USART2 ở chế độ Asynchronous. - Parameter Settings:
| Thông số | Giá trị |
|---|---|
| Baud Rate | 115200 |
| Word Length | 8 Bits (including Parity) |
| Parity | None |
| Stop Bits | 1 |
| Over Sampling | 16 Samples |
- Tab DMA Settings: bấm Add, chọn
USART2_RX. Đặt Mode = Circular, Data Width = Byte, bật Increment Address cho Memory. - Tab NVIC Settings: bật
USART2 global interrupt(ngắt IDLE cần cái này) và ngắt của kênh DMA tương ứng. - Clock Configuration: kiểm tra clock cấp cho USART và nguồn HSE.
- Generate Code.
6. Mã nguồn
6.1 Polling: echo đơn giản
Đoạn này chỉ để xác nhận nối dây và baudrate đúng trước khi làm gì khác:
uint8_t c;
while (1) {
if (HAL_UART_Receive(&huart2, &c, 1, HAL_MAX_DELAY) == HAL_OK)
HAL_UART_Transmit(&huart2, &c, 1, 10);
}
6.2 Ngắt: nhận từng byte
static uint8_t rx_byte;
void uart_start(void) { HAL_UART_Receive_IT(&huart2, &rx_byte, 1); }
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2) {
ring_put(rx_byte); // cất byte vào bộ đệm
HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // nhận tiếp
}
}
Ở tốc độ thấp cách này đủ dùng. Lên 921 600 baud thì cứ khoảng 10,9 µs lại có một ngắt, CPU bận đáng kể chỉ để chép byte.
6.3 DMA + IDLE: nhận gói dài bất kỳ
Hình 3. Luồng dữ liệu: DMA chép từng byte vào RAM mà không cần CPU. Khi đường RX im lặng, ngắt IDLE gọi callback một lần để xử lý phần dữ liệu mới.
#define RX_BUF_SIZE 256
static uint8_t rx_buf[RX_BUF_SIZE];
static volatile uint16_t rx_last = 0; // đã xử lý đến vị trí nào
void uart_start_rx(void)
{
rx_last = 0;
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buf, RX_BUF_SIZE);
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT); // bỏ ngắt nửa bộ đệm, chỉ giữ IDLE và đầy
}
// HAL gọi khi có sự kiện IDLE hoặc bộ đệm đầy.
// Size là vị trí GHI hiện tại của DMA trong bộ đệm vòng, không phải số byte lần này.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart != &huart2) return;
if (Size > rx_last) { // không quay vòng
process(&rx_buf[rx_last], Size - rx_last);
} else if (Size < rx_last) { // DMA đã quay vòng
process(&rx_buf[rx_last], RX_BUF_SIZE - rx_last);
if (Size) process(&rx_buf[0], Size);
}
rx_last = (Size == RX_BUF_SIZE) ? 0 : Size;
}
Hình 4. Dữ liệu mới nằm giữa rx_last và Size. Nếu DMA đã quay về đầu bộ đệm (Size nhỏ hơn rx_last), phải đọc hai đoạn: từ rx_last đến cuối, rồi từ đầu đến Size.
Với DMA Circular, đừng gọi lại HAL_UARTEx_ReceiveToIdle_DMA trong callback. Gọi lại sẽ đưa DMA về đầu bộ đệm và cách tính vị trí ở trên sai ngay.
Callback chạy trong ngắt, vì vậy process() chỉ nên chép dữ liệu hoặc đẩy vào hàng đợi, phần xử lý nặng để vòng lặp chính làm. Nếu cần phân biệt sự kiện nửa bộ đệm, đầy và IDLE, dùng HAL_UARTEx_GetRxEventType(huart) và giữ lại ngắt nửa bộ đệm.
Gói dài hơn bộ đệm vẫn được xử lý đúng: callback được gọi nhiều lần, Size tăng dần rồi quay vòng. Còn khi gặp lỗi như overrun, HAL có thể dừng nhận, nên hãy gọi lại uart_start_rx() trong HAL_UART_ErrorCallback. Hành vi này khác nhau giữa các phiên bản HAL, cần kiểm tra lại với bản bạn dùng.
6.4 Gửi bằng DMA
static volatile uint8_t tx_busy = 0;
int uart_send(const uint8_t *p, uint16_t n)
{
if (tx_busy) return -1; // lần gửi trước chưa xong
tx_busy = 1;
if (HAL_UART_Transmit_DMA(&huart2, (uint8_t *)p, n) != HAL_OK) { tx_busy = 0; return -1; }
return 0;
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2) tx_busy = 0;
}
Bộ đệm p phải giữ nguyên cho đến khi DMA gửi xong. Không truyền con trỏ tới biến cục bộ trên stack.
7. Lỗi thường gặp
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Ký tự rác | Sai baudrate hoặc khuôn dạng (parity, stop bit), clock không chính xác, thiếu chung GND | Kiểm tra cấu hình hai bên, dùng HSE, dùng logic analyzer giải mã UART |
| Mất byte, cờ Overrun (ORE) | CPU không kịp đọc ở polling hoặc ngắt | Chuyển sang DMA, tăng ưu tiên ngắt UART |
| Framing error (FE) liên tục | Chân RX để hở, khác mức điện áp, lệch baudrate | Nối đúng dây, kiểm tra mức logic và sai số baudrate |
| Chỉ nhận một phần gói rồi dừng | Quên DMA Circular, bộ đệm nhỏ hơn gói, gọi lại hàm nhận trong callback | Đặt Circular, tăng RX_BUF_SIZE, không gọi lại trong callback |
Chương trình đứng ở HAL_UART_Transmit |
Timeout quá lớn (HAL_MAX_DELAY) khi chưa có thiết bị nhận |
Dùng timeout hữu hạn hoặc gửi bằng DMA |
| Dữ liệu gửi bị sai nội dung | Truyền con trỏ biến cục bộ cho DMA, buffer bị ghi đè | Dùng bộ đệm tĩnh hoặc toàn cục |
8. Kế hoạch đo thực tế
Các phép đo dưới đây sẽ thay số tính bằng số đo. Cột "Đo thực tế" để trống cho đến khi chạy trên phần cứng.
| Thử nghiệm | Cách đo | Giá trị tính toán | Đo thực tế |
|---|---|---|---|
| Độ rộng 1 bit ở 115200 | Logic analyzer, đo bit start | 8,68 µs | (điền sau) |
| Sai số baudrate ở 921600 với HSE và HSI | Logic analyzer, đo độ rộng bit | +0,16% (với HSE, fck 84 MHz) | (điền sau) |
| Thông lượng tối đa ở 921600 | Gửi liên tục 1 MB, đo thời gian | 92 160 byte/s | (điền sau) |
| Tỷ lệ mất byte: polling, ngắt, DMA | Gửi 1 triệu byte có số thứ tự, kiểm tra bên nhận | Không mất ở DMA | (điền sau) |
| Tải CPU: ngắt so với DMA | Bật chân GPIO trong callback, đo duty cycle bằng oscilloscope | Không có | (điền sau) |
| Độ trễ từ byte cuối đến callback IDLE | Logic analyzer: so RX với chân GPIO trong callback | Khoảng một thời gian ký tự | (điền sau) |
9. Hỏi đáp
UART và USART khác nhau chỗ nào? USART có thêm chế độ đồng bộ với chân clock riêng. Ở chế độ bất đồng bộ thường dùng thì hai cái như nhau, và các khối USARTx trên STM32 chạy được cả hai chế độ.
Sao 115200 baud mà chỉ được khoảng 11 KB/s? Mỗi byte tốn 10 bit trên đường (1 start, 8 dữ liệu, 1 stop). 115200 chia 10 là 11 520 byte/s.
Có nên bật parity không? Parity chỉ phát hiện được lỗi một bit và không sửa được. Đường ngắn, sạch thì dùng 8N1 rồi kiểm tra toàn vẹn ở tầng giao thức bằng checksum hoặc CRC, như Modbus RTU ở phần 7.
Bộ đệm DMA nên lớn bao nhiêu? Đủ chứa dữ liệu đến trong khoảng giữa hai lần callback được xử lý, cộng thêm phần dư. Gói lớn nhất 128 byte thì chọn từ 256 byte trở lên. Bộ đệm nhỏ quá thì DMA sẽ ghi đè lên dữ liệu chưa kịp đọc.
Dùng RTOS thì sao? Trong callback, đẩy cặp (con trỏ, độ dài) vào hàng đợi hoặc báo semaphore, rồi để một task xử lý. Không gọi hàm chặn trong callback.
Kết luận
Trước khi chọn tốc độ, tính sai số baudrate với clock thực tế của chip. Cần độ tin cậy thì dùng HSE. Với dữ liệu liên tục, dùng DMA Circular cộng ngắt IDLE, và nhớ Size là vị trí ghi chứ không phải số byte.
Phần 2 chuyển sang SPI: giao thức đồng bộ, nhanh hơn UART nhiều, với bốn chế độ CPOL/CPHA cần chọn đúng theo thiết bị.
Để tính CRC hoặc giải mã byte nhận được, có thể dùng máy tính CRC và Hex Converter của Capitlab.
Tài liệu tham khảo
- STM32 UART Idle Line Detection: Interrupt & DMA Reception (ControllersTech)
- Reference manual và tài liệu STM32 HAL của dòng chip bạn dùng (mô tả USART, BRR và
HAL_UARTEx_ReceiveToIdle_DMA)