STM32 Non-Blocking UART Logger

Case study thiết kế module logging non-blocking cho STM32, hỗ trợ UART DMA, Ring Buffer, bare-metal, FreeRTOS và production build mode.

Cập nhật 5 phút đọc
Project STM32 Non-Blocking UART Logger dùng Ring Buffer và DMA để truyền log không chặn CPU.

Thông tin project

Trạng thái

Case study

Chuyên mục

Tech stack

STM32CubeIDESTM32 HALUART DMAFreeRTOS / CMSIS-RTOSGCC Linker

Đây là case study. Source code không được public vì liên quan đến dự án thực tế. Nội dung được viết ở mức kiến trúc và bài học kỹ thuật.

1. Mục tiêu project

Project này dùng để thiết kế một module debug log cho STM32 với mục tiêu:

  • Không block CPU lâu khi in log.
  • Không mất log khi UART DMA đang bận.
  • Không để log của nhiều RTOS task bị trộn ký tự vào nhau.
  • Có cổng log riêng cho ISR.
  • Có thể loại bỏ debug log khỏi binary khi build production.

2. Phạm vi phiên bản hiện tại

Phiên bản đầu tiên tập trung vào phần kiến trúc và code mẫu:

  • Có UART TX bằng DMA.
  • Có Ring Buffer quản lý queue log trong RAM.
  • Có bản bare-metal.
  • Có bản FreeRTOS/CMSIS-RTOS.
  • Có macro tắt log cho production build.

Chưa làm ở version này:

  • Chưa đóng gói thành thư viện .c/.h hoàn chỉnh.
  • Chưa benchmark thực tế bằng logic analyzer.
  • Chưa thêm log level như DEBUG, INFO, WARN, ERROR.
  • Chưa thêm counter thống kê số log bị drop.

3. Bối cảnh kỹ thuật

MCU: STM32 series bất kỳ có UART + DMA
IDE: STM32CubeIDE
HAL: STM32 HAL UART/DMA
RTOS: optional, FreeRTOS/CMSIS-RTOS
Output: UART TX tới USB-UART / TeraTerm
Build: GCC + linker garbage collection

4. Problem

printf() mặc định thường được redirect qua _write() và dùng HAL_UART_Transmit().

Vấn đề:

printf()

_write()

HAL_UART_Transmit()

CPU chờ UART truyền xong

Ở baudrate 115200 bps, một dòng log 60 ký tự có thể tốn khoảng 5.2 ms để truyền vật lý. Trong hệ thống real-time, đây là một khoảng thời gian rất lớn.

5. Architecture

Application / Task / ISR

Logger API

vsnprintf temporary buffer

Ring Buffer

DMA trigger

UART TX

Trách nhiệm từng phần:

Thành phầnTrách nhiệm
Logger APINhận format string và argument
vsnprintf()Gom format thành chuỗi hoàn chỉnh
Ring BufferLưu dữ liệu chờ truyền
DMATruyền dữ liệu ra UART ở background
CallbackCập nhật tail, clear dma_busy, gửi tiếp phần còn lại
Architecture của logger STM32 Ring Buffer DMA
Luồng xử lý chính của module logger.

6. Module dự kiến

Khi hoàn thiện thành source code thật, nên tách khỏi main.c như sau:

Core/
├── Inc/
│   └── debug_uart_logger.h

├── Src/
│   └── debug_uart_logger.c

API dự kiến:

void DebugLogger_Init(UART_HandleTypeDef *huart);
void DebugLogger_Printf(const char *format, ...);
void DebugLogger_PrintfFromISR(const char *format, ...);
void DebugLogger_TxCpltCallback(UART_HandleTypeDef *huart);
uint32_t DebugLogger_GetDroppedCount(void);

7. Luồng xử lý chính

Step 1: Task gọi printf()

Step 2: Logger format chuỗi bằng vsnprintf()

Step 3: Logger ghi chuỗi vào Ring Buffer

Step 4: Nếu DMA rảnh, start DMA TX

Step 5: DMA truyền xong và gọi callback

Step 6: Callback cập nhật tail và gửi tiếp nếu còn log

8. Bare-metal implementation

Trong bare-metal:

  • Dùng PRIMASK để tạo critical section ngắn.
  • Bảo vệ head, tail, dma_busy.
  • Không để callback xử lý logic dài.
uint32_t primask = __get_PRIMASK();
__disable_irq();

/* update head/tail/dma_busy */

__set_PRIMASK(primask);

9. RTOS implementation

Trong RTOS:

  • Task context dùng Mutex.
  • ISR context không dùng Mutex.
  • DMA callback không được block.
Ngữ cảnhCách bảo vệ
TaskosMutexAcquire() / osMutexRelease()
ISRtaskENTER_CRITICAL_FROM_ISR()
DMA callbackCritical section ngắn hoặc xử lý tối thiểu

10. Production build mode

Khi build production:

#ifndef ENABLE_DEBUG_LOG
#define printf(fmt, ...)     ((void)0)
#define printf_ISR(fmt, ...) ((void)0)
#endif

Kèm cấu hình compiler/linker:

-ffunction-sections
-fdata-sections
-Wl,--gc-sections

Mục tiêu:

  • Xóa lời gọi log khỏi binary.
  • Không giữ string log trong .rodata.
  • Loại bỏ hàm logger và buffer không còn được tham chiếu.
  • Có thể tắt clock UART nếu UART chỉ dùng để debug.

11. Debug và lỗi thường gặp

Lỗi 1: Mất ký tự khi dùng DMA

Nguyên nhân có thể:

  • Gọi HAL_UART_Transmit_DMA() liên tục khi DMA đang bận.
  • Không kiểm tra HAL_BUSY.
  • Không có queue/Ring Buffer ở giữa.

Lỗi 2: Log của nhiều task bị trộn

Nguyên nhân có thể:

  • Nhiều task cùng ghi vào buffer mà không có Mutex.
  • Một message bị chia nhỏ rồi bị task khác chen vào.

Lỗi 3: Production vẫn còn tốn RAM/Flash vì logger

Nguyên nhân có thể:

  • Chỉ tắt macro printf nhưng chưa bật linker garbage collection.
  • Biến global logger vẫn bị tham chiếu ở đâu đó.
  • String log vẫn nằm trong code khác ngoài macro.

12. Checklist triển khai

13. Kết quả hiện tại

Hiện project đang ở mức case study/template:

  • Đã có kiến trúc tổng thể.
  • Đã có code mẫu inline trong blog.
  • Đã có hướng tách module.
  • Chưa benchmark trên hardware thật.

14. Bài liên quan nên đọc tiếp

15. Bài học rút ra

Logger trong embedded không chỉ là một hàm in chuỗi. Nó là một phần của kiến trúc hệ thống.

Một logger tốt cần nghĩ đến timing, context, DMA state, memory usage, production build và cách debug sau này.

Thấy nội dung này hữu ích?

Lưu lại hoặc chia sẻ cho người cũng đang học firmware, BIOS/UEFI và embedded systems.

Nội dung liên quan

Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.

Tiếp tục xem các project embedded

Các project thực chiến giúp biến ghi chú kỹ thuật thành kinh nghiệm.