#Khung tin UART cơ bản và vai trò của SOF
Start of Frame (SOF) là byte đặc biệt (thường 0x7E) dùng để đồng bộ hoá bộ nhận khi không có đồng hồ chung. Khi MCU nhận được SOF, nó bắt đầu đếm các byte tiếp theo để xây dựng một khung tin hoàn chỉnh.
Cấu trúc tiêu chuẩn (đơn giản nhất)
| SOF | Length | Payload … | CRC | | 1B | 1B | N bytes | 1‑2B |
- SOF: đánh dấu đầu khung, giúp bộ nhận bỏ qua các byte nhiễu.
- Length: số byte của Payload (không tính CRC). Tránh phải dựa vào ký tự kết thúc (VD: '\n') vì có thể xuất hiện trong dữ liệu nhị phân.
- Payload: dữ liệu thực tế (lệnh, giá trị cảm biến…).
- CRC: kiểm tra toàn bộ Payload (và thường cả Length).
#Sơ đồ khung tin
+------+--------+-------------------+------+
| SOF | Length | Payload | CRC |
| 0x7E | 0x0A | 0x12 0x34 … 0x9F | 0x5C |
+------+--------+-------------------+------+#Kiểm tra tính toàn vẹn: CRC vs Checksum đơn giản
| Thuật toán | Độ dài | Phát hiện lỗi đơn bit | Phát hiện lỗi burst ≤ n bit | Tốc độ tính (CPU cycles) | Đề xuất đa thức |
|---|---|---|---|---|---|
| XOR (Sum) | 1 B | 50 % ≈ 1‑bit error | kém (≈ 0 %) | ~5 cycles/byte | — |
| CRC‑8 | 1 B | 99 % ≈ 1‑bit error | 100 % ≤ 8 bit burst | ~12 cycles/byte | x⁸ + x² + x + 1 (poly 0x07) – STM32 CRC‑8 HW |
| CRC‑16‑CCITT | 2 B | 99.9 % ≈ 1‑bit error | 100 % ≤ 16 bit burst | ~20 cycles/byte | x¹⁶ + x¹² + x⁵ + 1 (poly 0x1021) – STM32 CRC‑16 HW |
Trường CRC luôn đặt sau Payload để bao trùm toàn bộ dữ liệu truyền.
#Vấn đề Null‑terminator trong chuỗi lệnh
Khi truyền một chuỗi C (char cmd[] = "SET_LED";) qua UART, nếu buffer nhận chỉ có chỗ cho 7 byte (độ dài chuỗi) mà không dự trữ thêm 1 byte cho '\0', hàm strlen() hoặc printf() sẽ đọc vượt ra ngoài vùng nhớ, gây buffer overflow.
#Ví dụ lỗi overflow
/* minh hoạ, chưa biên dịch */
#define RX_BUF_SIZE 8 // 7 byte payload + 1 byte cho '\0' (đúng)
uint8_t rx_buf[RX_BUF_SIZE];
void uart_rx_callback(uint8_t b)
{
static uint8_t idx = 0;
if (idx < RX_BUF_SIZE) {
rx_buf[idx++] = b;
} else {
// vượt tràn, không có chỗ cho '\0'
}
}
/* Khi xử lý */
char *cmd = (char *)rx_buf; // giả sử payload là chuỗi
printf("Command: %s\n", cmd); // có thể đọc tới vùng nhớ không thuộc rx_buf#Trần kích thước bộ đệm (Buffer Size Limit)
| Buffer size | RAM dùng cho buffer (Byte) | Max Payload (Byte) | ISR latency (µs) @115.2 kbps |
|---|---|---|---|
| 256 B | 256 B | 252 B (4 B header) | ~22 µs (≈ 256 byte / 115.2 kbps) |
| 512 B | 512 B | 508 B | ~44 µs |
| 1 KB | 1 024 B | 1 020 B | ~88 µs |
RAM khả dụng được tính dựa trên MCU STM32F103 (SRAM 20 KB) – dành 5 % cho các task RTOS, còn lại cho UART buffer.
/* Định nghĩa hằng số payload tối đa */
#define MAX_PAYLOAD_SIZE 508U // 20 ms (~230 byte ở 115200 baud) đủ để phát hiện ngắt quãng dữ liệu giữa frame#Xử lý ngoại lệ khi vượt trần payload
- Drop frame – Bỏ khung tin và gửi ACK lỗi (
NACK) về phía gửi. - Truncate – Cắt payload tới
MAX_PAYLOAD_SIZEvà đánh dấu lỗi (ERR_TRUNC). Không khuyến nghị cho dữ liệu quan trọng.
Cơ chế phát hiện: so sánh Length trong header với MAX_PAYLOAD_SIZE. Nếu Length > MAX_PAYLOAD_SIZE → kích hoạt quy trình lỗi.
#Flowchart (text)
[Receive SOF] → [Read Length] →
Length <= MAX_PAYLOAD_SIZE ? → Yes → [Read Payload] → [Read CRC] → [Validate] → OK
No → [Discard bytes] → [Send NACK] → End#Chúng tôi đã gặp gì trên thiết bị thật
Log mẫu (đã ẩn danh)
[2026-09-18 14:32:07] UART_RX: Length=1024 > MAX=512 → Drop frame, NACK sent
[2026-09-19 09:11:22] CMD_PROC: strcmp() overrun, possible null‑terminator missing#Checklist khi thiết kế
- Buffer nhận có đủ chỗ cho Header + Payload + CRC + null‑terminator?
- Kiểm tra trường hợp frame rỗng (Length = 0) hoặc frame quá dài (Length > MAX_PAYLOAD_SIZE).
- CRC được tính trên toàn bộ Payload (và Length nếu cần).
- Thực hiện test biên giới: payload = 0, 1, MAX, MAX+1 byte.
- Đảm bảo timeout nhận dữ liệu để phát hiện khung tin bị cắt.
- Khi dùng DMA, kích thước circular buffer phải ≥ 2 × MAX_PAYLOAD_SIZE.
#Tối ưu hiệu năng và độ tin cậy
#Cấu hình UART DMA (STM32 HAL)
/* minh hoạ, chưa biên dịch */
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_rx;
/* UART init */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
/* DMA circular buffer */
uint8_t uart_rx_buf[2 * MAX_PAYLOAD_SIZE];
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));#Double buffering trong ISR
/* minh hoạ, chưa biên dịch */
static uint8_t buf_a[MAX_PAYLOAD_SIZE];
static uint8_t buf_b[MAX_PAYLOAD_SIZE];
static uint8_t *active_buf = buf_a;
static uint8_t *proc_buf = buf_b;
void UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Đảo con trỏ, giao buffer cho task xử lý */
uint8_t *tmp = active_buf;
active_buf = proc_buf;
proc_buf = tmp;
/* Bắt đầu DMA cho buffer mới */
HAL_UART_Receive_DMA(huart, active_buf, MAX_PAYLOAD_SIZE);
/* Signal task xử lý */
osMessagePut(proc_queue, (uint32_t)proc_buf, 0);
}#Timeout nhận dữ liệu
/* minh hoạ, chưa biên dịch */
#define FRAME_TIMEOUT_MS 20U // 20 ms (~230 byte ở 115200 baud) đủ để phát hiện ngắt quãng dữ liệu giữa frame
void uart_rx_task(void const *arg)
{
uint32_t tick_start = osKernelGetTickCount();
while (1) {
if (osMessageAvailable(proc_queue)) {
/* Xử lý frame */
tick_start = osKernelGetTickCount();
} else if (osKernelGetTickCount() - tick_start > FRAME_TIMEOUT_MS) {
/* Frame bị cắt, reset trạng thái */
reset_uart_state();
tick_start = osKernelGetTickCount();
}
}
}#Đọc tiếp
- Bài trước trong lộ trình: GPIO thực chiến: open-drain, pull-up, chống dội, ESD
- Bài kế tiếp trong Series: Đo dòng bằng shunt + INA226: độ phân giải, bão hòa, chọn shunt
- Bài nâng cao: FreeRTOS + ISR + UART: queue, watchdog, và xử lý deadlock
Mục lục bài viết15 ▼
- Khung tin UART cơ bản và vai trò của SOF
- Sơ đồ khung tin
- Kiểm tra tính toàn vẹn: CRC vs Checksum đơn giản
- Vấn đề Null‑terminator trong chuỗi lệnh
- Ví dụ lỗi overflow
- Trần kích thước bộ đệm (Buffer Size Limit)
- Xử lý ngoại lệ khi vượt trần payload
- Flowchart (text)
- Chúng tôi đã gặp gì trên thiết bị thật
- Checklist khi thiết kế
- Tối ưu hiệu năng và độ tin cậy
- Cấu hình UART DMA (STM32 HAL)
- Double buffering trong ISR
- Timeout nhận dữ liệu
- Đọc tiếp
#Khung tin UART cơ bản và vai trò của SOF
Start of Frame (SOF) là byte đặc biệt (thường 0x7E) dùng để đồng bộ hoá bộ nhận khi không có đồng hồ chung. Khi MCU nhận được SOF, nó bắt đầu đếm các byte tiếp theo để xây dựng một khung tin hoàn chỉnh.
Cấu trúc tiêu chuẩn (đơn giản nhất)
| SOF | Length | Payload … | CRC | | 1B | 1B | N bytes | 1‑2B |
- SOF: đánh dấu đầu khung, giúp bộ nhận bỏ qua các byte nhiễu.
- Length: số byte của Payload (không tính CRC). Tránh phải dựa vào ký tự kết thúc (VD: '\n') vì có thể xuất hiện trong dữ liệu nhị phân.
- Payload: dữ liệu thực tế (lệnh, giá trị cảm biến…).
- CRC: kiểm tra toàn bộ Payload (và thường cả Length).
#Sơ đồ khung tin
+------+--------+-------------------+------+
| SOF | Length | Payload | CRC |
| 0x7E | 0x0A | 0x12 0x34 … 0x9F | 0x5C |
+------+--------+-------------------+------+#Kiểm tra tính toàn vẹn: CRC vs Checksum đơn giản
| Thuật toán | Độ dài | Phát hiện lỗi đơn bit | Phát hiện lỗi burst ≤ n bit | Tốc độ tính (CPU cycles) | Đề xuất đa thức |
|---|---|---|---|---|---|
| XOR (Sum) | 1 B | 50 % ≈ 1‑bit error | kém (≈ 0 %) | ~5 cycles/byte | — |
| CRC‑8 | 1 B | 99 % ≈ 1‑bit error | 100 % ≤ 8 bit burst | ~12 cycles/byte | x⁸ + x² + x + 1 (poly 0x07) – STM32 CRC‑8 HW |
| CRC‑16‑CCITT | 2 B | 99.9 % ≈ 1‑bit error | 100 % ≤ 16 bit burst | ~20 cycles/byte | x¹⁶ + x¹² + x⁵ + 1 (poly 0x1021) – STM32 CRC‑16 HW |
Trường CRC luôn đặt sau Payload để bao trùm toàn bộ dữ liệu truyền.
#Vấn đề Null‑terminator trong chuỗi lệnh
Khi truyền một chuỗi C (char cmd[] = "SET_LED";) qua UART, nếu buffer nhận chỉ có chỗ cho 7 byte (độ dài chuỗi) mà không dự trữ thêm 1 byte cho '\0', hàm strlen() hoặc printf() sẽ đọc vượt ra ngoài vùng nhớ, gây buffer overflow.
#Ví dụ lỗi overflow
/* minh hoạ, chưa biên dịch */
#define RX_BUF_SIZE 8 // 7 byte payload + 1 byte cho '\0' (đúng)
uint8_t rx_buf[RX_BUF_SIZE];
void uart_rx_callback(uint8_t b)
{
static uint8_t idx = 0;
if (idx < RX_BUF_SIZE) {
rx_buf[idx++] = b;
} else {
// vượt tràn, không có chỗ cho '\0'
}
}
/* Khi xử lý */
char *cmd = (char *)rx_buf; // giả sử payload là chuỗi
printf("Command: %s\n", cmd); // có thể đọc tới vùng nhớ không thuộc rx_buf#Trần kích thước bộ đệm (Buffer Size Limit)
| Buffer size | RAM dùng cho buffer (Byte) | Max Payload (Byte) | ISR latency (µs) @115.2 kbps |
|---|---|---|---|
| 256 B | 256 B | 252 B (4 B header) | ~22 µs (≈ 256 byte / 115.2 kbps) |
| 512 B | 512 B | 508 B | ~44 µs |
| 1 KB | 1 024 B | 1 020 B | ~88 µs |
RAM khả dụng được tính dựa trên MCU STM32F103 (SRAM 20 KB) – dành 5 % cho các task RTOS, còn lại cho UART buffer.
/* Định nghĩa hằng số payload tối đa */
#define MAX_PAYLOAD_SIZE 508U // 20 ms (~230 byte ở 115200 baud) đủ để phát hiện ngắt quãng dữ liệu giữa frame#Xử lý ngoại lệ khi vượt trần payload
- Drop frame – Bỏ khung tin và gửi ACK lỗi (
NACK) về phía gửi. - Truncate – Cắt payload tới
MAX_PAYLOAD_SIZEvà đánh dấu lỗi (ERR_TRUNC). Không khuyến nghị cho dữ liệu quan trọng.
Cơ chế phát hiện: so sánh Length trong header với MAX_PAYLOAD_SIZE. Nếu Length > MAX_PAYLOAD_SIZE → kích hoạt quy trình lỗi.
#Flowchart (text)
[Receive SOF] → [Read Length] →
Length <= MAX_PAYLOAD_SIZE ? → Yes → [Read Payload] → [Read CRC] → [Validate] → OK
No → [Discard bytes] → [Send NACK] → End#Chúng tôi đã gặp gì trên thiết bị thật
Log mẫu (đã ẩn danh)
[2026-09-18 14:32:07] UART_RX: Length=1024 > MAX=512 → Drop frame, NACK sent
[2026-09-19 09:11:22] CMD_PROC: strcmp() overrun, possible null‑terminator missing#Checklist khi thiết kế
- Buffer nhận có đủ chỗ cho Header + Payload + CRC + null‑terminator?
- Kiểm tra trường hợp frame rỗng (Length = 0) hoặc frame quá dài (Length > MAX_PAYLOAD_SIZE).
- CRC được tính trên toàn bộ Payload (và Length nếu cần).
- Thực hiện test biên giới: payload = 0, 1, MAX, MAX+1 byte.
- Đảm bảo timeout nhận dữ liệu để phát hiện khung tin bị cắt.
- Khi dùng DMA, kích thước circular buffer phải ≥ 2 × MAX_PAYLOAD_SIZE.
#Tối ưu hiệu năng và độ tin cậy
#Cấu hình UART DMA (STM32 HAL)
/* minh hoạ, chưa biên dịch */
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_rx;
/* UART init */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
/* DMA circular buffer */
uint8_t uart_rx_buf[2 * MAX_PAYLOAD_SIZE];
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));#Double buffering trong ISR
/* minh hoạ, chưa biên dịch */
static uint8_t buf_a[MAX_PAYLOAD_SIZE];
static uint8_t buf_b[MAX_PAYLOAD_SIZE];
static uint8_t *active_buf = buf_a;
static uint8_t *proc_buf = buf_b;
void UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Đảo con trỏ, giao buffer cho task xử lý */
uint8_t *tmp = active_buf;
active_buf = proc_buf;
proc_buf = tmp;
/* Bắt đầu DMA cho buffer mới */
HAL_UART_Receive_DMA(huart, active_buf, MAX_PAYLOAD_SIZE);
/* Signal task xử lý */
osMessagePut(proc_queue, (uint32_t)proc_buf, 0);
}#Timeout nhận dữ liệu
/* minh hoạ, chưa biên dịch */
#define FRAME_TIMEOUT_MS 20U // 20 ms (~230 byte ở 115200 baud) đủ để phát hiện ngắt quãng dữ liệu giữa frame
void uart_rx_task(void const *arg)
{
uint32_t tick_start = osKernelGetTickCount();
while (1) {
if (osMessageAvailable(proc_queue)) {
/* Xử lý frame */
tick_start = osKernelGetTickCount();
} else if (osKernelGetTickCount() - tick_start > FRAME_TIMEOUT_MS) {
/* Frame bị cắt, reset trạng thái */
reset_uart_state();
tick_start = osKernelGetTickCount();
}
}
}#Đọc tiếp
- Bài trước trong lộ trình: GPIO thực chiến: open-drain, pull-up, chống dội, ESD
- Bài kế tiếp trong Series: Đo dòng bằng shunt + INA226: độ phân giải, bão hòa, chọn shunt
- Bài nâng cao: FreeRTOS + ISR + UART: queue, watchdog, và xử lý deadlock