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.
Thông tin project
Trạng thái
Chuyên mục
Tech stack
Đâ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/.hhoà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ần | Trách nhiệm |
|---|---|
| Logger API | Nhận format string và argument |
vsnprintf() | Gom format thành chuỗi hoàn chỉnh |
| Ring Buffer | Lưu dữ liệu chờ truyền |
| DMA | Truyền dữ liệu ra UART ở background |
| Callback | Cập nhật tail, clear dma_busy, gửi tiếp phần còn lại |
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ảnh | Cách bảo vệ |
|---|---|
| Task | osMutexAcquire() / osMutexRelease() |
| ISR | taskENTER_CRITICAL_FROM_ISR() |
| DMA callback | Critical 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
printfnhư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
- Bài blog chính
- Ring Buffer là gì trong firmware embedded?
- DMA Normal mode và Circular mode khác nhau thế nào?
- Dead Code Elimination là gì trong embedded C?
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.
UART non-blocking logger trên STM32 với DMA + Ring Buffer
Thiết kế debug UART non-blocking cho STM32: tránh printf, xử lý mất log với DMA, dùng ring buffer và DMA callback.
DMA Normal mode và Circular mode khác nhau thế nào?
Phân biệt DMA Normal mode và Circular mode khi dùng UART, ADC, audio stream hoặc logger trên STM32.
HAL_BUSY xảy ra khi nào trong UART DMA?
Giải thích lỗi HAL_BUSY khi gọi HAL_UART_Transmit_DMA() trong lúc UART/DMA vẫn đang truyền dữ liệu trướ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.