Trung bình4 phút đọc22/09/2026

UART giữa hai MCU: khung tin, checksum, và trần payload

Thiết kế khung tin (SOF/độ dài/CRC), null-terminator, trần kích thước bộ đệm và điều gì xảy ra khi vượt trần.

UART giữa hai MCU: khung tin, checksum, và trần payload

#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

text
+------+--------+-------------------+------+
| 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àiPhát hiện lỗi đơn bitPhát hiện lỗi burst ≤ n bitTốc độ tính (CPU cycles)Đề xuất đa thức
XOR (Sum)1 B50 % ≈ 1‑bit errorkém (≈ 0 %)~5 cycles/byte
CRC‑81 B99 % ≈ 1‑bit error100 % ≤ 8 bit burst~12 cycles/bytex⁸ + x² + x + 1 (poly 0x07) – STM32 CRC‑8 HW
CRC‑16‑CCITT2 B99.9 % ≈ 1‑bit error100 % ≤ 16 bit burst~20 cycles/bytex¹⁶ + 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

C
/* 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 sizeRAM dùng cho buffer (Byte)Max Payload (Byte)ISR latency (µs) @115.2 kbps
256 B256 B252 B (4 B header)~22 µs (≈ 256 byte / 115.2 kbps)
512 B512 B508 B~44 µs
1 KB1 024 B1 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.

C
/* Đị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

  1. Drop frame – Bỏ khung tin và gửi ACK lỗi (NACK) về phía gửi.
  2. Truncate – Cắt payload tới MAX_PAYLOAD_SIZE và đá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)

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)

text
[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)

C
/* 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

C
/* 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

C
/* 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

Mục lục bài viết15
  1. Khung tin UART cơ bản và vai trò của SOF
  2. Sơ đồ khung tin
  3. Kiểm tra tính toàn vẹn: CRC vs Checksum đơn giản
  4. Vấn đề Null‑terminator trong chuỗi lệnh
  5. Ví dụ lỗi overflow
  6. Trần kích thước bộ đệm (Buffer Size Limit)
  7. Xử lý ngoại lệ khi vượt trần payload
  8. Flowchart (text)
  9. Chúng tôi đã gặp gì trên thiết bị thật
  10. Checklist khi thiết kế
  11. Tối ưu hiệu năng và độ tin cậy
  12. Cấu hình UART DMA (STM32 HAL)
  13. Double buffering trong ISR
  14. Timeout nhận dữ liệu
  15. Đọ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

text
+------+--------+-------------------+------+
| 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àiPhát hiện lỗi đơn bitPhát hiện lỗi burst ≤ n bitTốc độ tính (CPU cycles)Đề xuất đa thức
XOR (Sum)1 B50 % ≈ 1‑bit errorkém (≈ 0 %)~5 cycles/byte
CRC‑81 B99 % ≈ 1‑bit error100 % ≤ 8 bit burst~12 cycles/bytex⁸ + x² + x + 1 (poly 0x07) – STM32 CRC‑8 HW
CRC‑16‑CCITT2 B99.9 % ≈ 1‑bit error100 % ≤ 16 bit burst~20 cycles/bytex¹⁶ + 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

C
/* 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 sizeRAM dùng cho buffer (Byte)Max Payload (Byte)ISR latency (µs) @115.2 kbps
256 B256 B252 B (4 B header)~22 µs (≈ 256 byte / 115.2 kbps)
512 B512 B508 B~44 µs
1 KB1 024 B1 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.

C
/* Đị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

  1. Drop frame – Bỏ khung tin và gửi ACK lỗi (NACK) về phía gửi.
  2. Truncate – Cắt payload tới MAX_PAYLOAD_SIZE và đá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)

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)

text
[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)

C
/* 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

C
/* 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

C
/* 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