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.
1. Khi printf() không còn là bạn hiền
Nếu bạn nghĩ printf() vô hại, bài này có thể khiến bạn đổi ý.
Trong firmware, log là thứ rất dễ bị xem nhẹ. Người mới dùng log để biết code có chạy không. Người có kinh nghiệm dùng log để bắt timing, trạng thái bus, command từ host, dữ liệu thô từ sensor, hoặc những bug “lúc có lúc không” mà breakpoint không bắt được. Nói thật, không có log thì nhiều lúc debug firmware giống như đi trong phòng tối, tay cầm mỗi cái tua vít.
Nhưng log cũng có mặt tối của nó. Trong một dự án đọc dữ liệu thẻ từ, tôi cần dump khoảng vài trăm bit raw data sau mỗi lần quẹt thẻ để so sánh với máy phân tích chuyên dụng. Khi tắt log, dữ liệu decode đúng. Khi bật log, dữ liệu bắt đầu lệch, thứ tự xử lý sai, thậm chí có lúc tôi nghi ngờ cả phần interrupt đọc tín hiệu bị lỗi.
Sau vài vòng tự nghi ngờ nhân phẩm, tôi nhận ra thủ phạm không phải thuật toán decode. Thủ phạm là chính dòng log tôi thêm vào để “xem cho chắc”.
printf() không sai nhưng cách tôi đưa printf() ra UART có vấn đề.
Bài này không chỉ hướng dẫn redirect printf() ra UART mà còn đi sâu vào cách thiết kế một logger đúng nghĩa cho STM32: có hàng đợi, có DMA, có policy khi buffer đầy, có API riêng cho task/ISR, và có cách biến log thành “hư vô” khi build production.
Phần code đầy đủ, cấu trúc module .c/.h, cách cấu hình CubeMX và demo thực hành sẽ được đặt trong project riêng:
STM32 Non-Blocking UART Logger.
2. Bức tranh tổng thể: logger không phải một dòng redirect
Một logger UART non-blocking nên được nhìn như một pipeline:
Application / Task / ISR
↓
Logger API
↓
Temporary formatting buffer hoặc raw string
↓
Ring Buffer trong RAM
↓
DMA TX engine
↓
UART TX pin
↓
USB-UART / TeraTerm / logic analyzer
Trong kiến trúc này:
printf()hoặcLOG_INFO()chỉ format/gom message.- Ring Buffer đóng vai trò hàng đợi giữa CPU nhanh và UART chậm.
- DMA truyền dữ liệu ra UART ở background.
- Callback của DMA/UART kéo tiếp dữ liệu còn chờ trong buffer.
- Production build có thể loại bỏ toàn bộ log khỏi binary.
Nói ngắn gọn: UART logger không nên là một hàm in chuỗi. Nó nên là một producer-consumer system nhỏ nằm trong firmware.
Đọc thêm note nền: Producer-Consumer pattern trong hệ thống logging embedded.
3. Redirect printf() mặc định hoạt động thế nào?
Trong STM32CubeIDE dùng GCC/newlib hoặc newlib-nano, cách phổ biến nhất để đưa printf() ra UART là override _write():
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}
Về ý tưởng, thư viện C xử lý format string trước. Sau đó nó gọi syscall _write() để ghi bytes ra output. Trên firmware bare-metal, ta tự quyết định output đó là gì: UART, SWO, semihosting, USB CDC, hoặc một kênh debug khác.
Điểm nguy hiểm nằm ở dòng này:
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
HAL_UART_Transmit() là API truyền kiểu polling/blocking. CPU phải chờ cho đến khi UART truyền xong hoặc timeout xảy ra. Trong thời gian đó, code chính không chạy tiếp.
3.1 UART chậm hơn cảm giác của mình rất nhiều
Với cấu hình UART phổ biến 115200 bps, một byte thường đi qua frame:
1 start bit + 8 data bits + 1 stop bit = 10 bits
Thời gian truyền một byte:
10 / 115200 ≈ 86.8 µs
Nếu một dòng log dài 60 ký tự:
60 x 86.8 µs ≈ 5.2 ms
5.2 ms nghe không lớn nếu bạn viết app desktop. Nhưng trong firmware, đó có thể là cả một đời người của hệ thống:
| Thành phần | Mốc thời gian thường gặp |
|---|---|
| FreeRTOS tick | 1 ms |
| Vòng đọc sensor nhanh | vài chục đến vài trăm µs |
| USB SOF full-speed | 1 ms |
| Một số xử lý interrupt | cần kết thúc càng nhanh càng tốt |
| Watchdog | có thể reset nếu flow bị kẹt lâu |
Vì vậy, một dòng log tưởng như vô hại có thể tạo jitter, làm task trễ deadline, khiến buffer ngoại vi overflow, hoặc làm bạn debug sai hướng.
Đọc thêm note tính toán: Cách tính thời gian truyền UART từ baudrate.
3.2 Khi lỗi chỉ xuất hiện lúc bật log
Đây là kiểu bug rất khó chịu:
Tắt log → hệ thống chạy đúng
Bật log → hệ thống chạy sai
Đặt breakpoint → lỗi biến mất
Tối về nhà → lỗi hiện lại
Trong dự án đọc thẻ từ, tôi muốn dump raw bit để đối chiếu. Ý định rất hợp lý: có dữ liệu thô thì sẽ phân tích được chính xác hơn. Nhưng vì dump quá nhiều dữ liệu qua UART blocking, firmware bị kéo chậm đúng lúc luồng xử lý cần giữ timing. Kết quả là log làm thay đổi hành vi hệ thống.
Đây là bài học đầu tiên: debug method không được làm biến dạng system behavior quá nhiều. Log càng gần timing-critical path, càng phải cẩn thận.
4. Vì sao “đổi sang DMA cho nhanh” vẫn chưa đủ?
Sau khi phát hiện polling UART block CPU, phản xạ tự nhiên là đổi sang DMA:
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)ptr, len);
return len;
}
Nhìn qua thì rất đẹp:
- CPU không ngồi chờ từng byte.
- DMA truyền ở background.
- Code sửa ít.
- Terminal vẫn có log.
Nhưng đây vẫn là lỗi kiến trúc.
DMA không phải queue. DMA chỉ nhận một vùng nhớ, truyền vùng nhớ đó, rồi báo hoàn tất.
Trong lúc DMA đang truyền, nếu bạn gọi HAL_UART_Transmit_DMA() lần nữa, HAL có thể trả về HAL_BUSY.
Nếu bạn bỏ qua return value, log sau sẽ mất mà không ai báo cho bạn biết.
printf("ABC")
↓
_write("A") → DMA start → OK
_write("B") → DMA busy → HAL_BUSY → mất B
_write("C") → DMA busy → HAL_BUSY → mất C
Đọc thêm note liên quan: HAL_BUSY xảy ra khi nào trong UART DMA?.
4.1 Sai lầm còn phổ biến hơn: transmit từng ký tự bằng polling
Một số tutorial redirect printf() theo kiểu:
int __io_putchar(uint8_t ch)
{
HAL_UART_Transmit(&huart2, &ch, 1, 0xFFFF);
return ch;
}
int _write(int file, char *ptr, int len)
{
for (int i = 0; i < len; i++) {
__io_putchar((uint8_t)*ptr++);
}
return len;
}
Cách này dễ hiểu, dễ chạy, nhưng cực kỳ đắt:
- mỗi ký tự gọi một lần
HAL_UART_Transmit(); - mỗi lần transmit lại polling;
- một dòng 60 ký tự thành 60 lần block nhỏ;
- tổng thời gian vẫn bị giới hạn bởi baudrate UART;
- overhead function call/state check tăng thêm.
Nó phù hợp cho “Hello World trên board mới mua”, không phù hợp cho firmware có timing thật.
5. Kiến trúc đúng: tách tốc độ CPU và tốc độ UART
Vấn đề cốt lõi là CPU tạo log nhanh hơn UART truyền log. Vậy ta cần một lớp đệm ở giữa.
Producer: CPU / task / ISR tạo log
Buffer : Ring Buffer trong RAM
Consumer: DMA + UART truyền log ra ngoài
Tư duy thiết kế
Đừng hỏi “làm sao để printf() gọi DMA?”. Hãy hỏi “log được xếp hàng ở đâu nếu UART đang bận?”.
Khi câu hỏi đổi như vậy, Ring Buffer gần như tự nhiên xuất hiện.
Một kiến trúc tối thiểu nên có 5 phần:
| Thành phần | Trách nhiệm |
|---|---|
| Logger API | Nhận message từ application/task/ISR |
| Format buffer | Gom format string thành một message hoàn chỉnh |
| Ring Buffer | Lưu log đang chờ truyền |
| DMA TX engine | Start DMA khi UART rảnh |
| Tx complete callback | Cập nhật tail, clear busy state, gửi tiếp chunk còn lại |
Đọc thêm note: Ring Buffer là gì trong firmware embedded?.
6. Ring Buffer trong logger: dễ viết, nhưng cũng dễ sai
Ring Buffer dùng một mảng cố định và hai chỉ số:
head = vị trí ghi byte mới
tail = vị trí đọc/truyền byte cũ
Quy ước thường dùng:
head == tail → buffer trống
next(head) == tail → buffer đầy
Vì head == tail đã dùng cho trạng thái empty, ta thường chừa lại 1 byte để phân biệt empty và full.
#define LOG_RING_SIZE 2048U
static uint8_t ring[LOG_RING_SIZE];
static volatile uint32_t head = 0;
static volatile uint32_t tail = 0;
static uint32_t rb_next(uint32_t index)
{
return (index + 1U) % LOG_RING_SIZE;
}
Case 1: dữ liệu liên tục, DMA có thể gửi một chunk thẳng
Case 2: dữ liệu wrap-around, DMA nên gửi phần tail → end trước
6.1 Free space: chỗ quyết định drop hay nhận log
Một công thức hay dùng:
static uint32_t rb_used(void)
{
if (head >= tail) {
return head - tail;
}
return LOG_RING_SIZE - tail + head;
}
static uint32_t rb_free(void)
{
return LOG_RING_SIZE - rb_used() - 1U;
}
Nếu message dài hơn free space, bạn phải chọn policy: drop byte, drop message, overwrite old log, block chờ, hoặc ưu tiên log quan trọng. Không chọn policy cũng là một policy - thường là policy tệ nhất.
6.2 Bẫy nghiêm trọng: callback không được tail = head
Đây là lỗi tôi muốn nhấn mạnh, vì nó nhìn rất “hợp lý” nhưng có thể làm mất log.
Giả sử DMA đang truyền đoạn từ tail đến tail + tx_len.
Trong lúc DMA truyền, task khác ghi thêm log vào buffer, làm head tiến lên. Nếu callback làm thế này:
// Sai trong nhiều trường hợp
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
tail = head;
dma_busy = 0;
}
thì callback có thể nhảy tail qua cả phần log mới được ghi trong lúc DMA đang bận. Nói cách khác: dữ liệu mới chưa được DMA gửi, nhưng bạn đã đánh dấu là “đã gửi”.
Log biến mất rất sạch sẽ, sạch đến mức khó debug.
Cách đúng là lưu lại độ dài chunk đang truyền khi start DMA, rồi callback chỉ advance đúng số byte đó:
static volatile uint8_t dma_busy = 0;
static volatile uint32_t dma_tx_len = 0;
static uint32_t rb_contiguous_len(void)
{
if (head >= tail) {
return head - tail;
}
return LOG_RING_SIZE - tail;
}
static void logger_kick_tx(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
if (dma_busy || (head == tail)) {
__set_PRIMASK(primask);
return;
}
uint32_t start = tail;
uint32_t len = rb_contiguous_len();
dma_busy = 1U;
dma_tx_len = len;
__set_PRIMASK(primask);
if (HAL_UART_Transmit_DMA(&huart1, &ring[start], len) != HAL_OK) {
primask = __get_PRIMASK();
__disable_irq();
dma_busy = 0U;
dma_tx_len = 0U;
__set_PRIMASK(primask);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance != USART1) {
return;
}
uint32_t primask = __get_PRIMASK();
__disable_irq();
tail = (tail + dma_tx_len) % LOG_RING_SIZE;
dma_tx_len = 0U;
dma_busy = 0U;
__set_PRIMASK(primask);
logger_kick_tx();
}
Phần project sẽ trình bày bản module hoàn chỉnh hơn: tách file .c/.h, thêm counter drop, test với wrap-around và stress log. Xem: Project implementation.
7. Không phải context nào cũng được log giống nhau
Một logger dùng tốt trong while(1) chưa chắc dùng an toàn trong RTOS.
Một logger dùng trong task chưa chắc được phép gọi trong ISR. Đây là kinh nghiệm thực tế mà tôi rút ra được sau nhiều lần “thụt hố”.
7.1 Bare-metal: critical section ngắn là đủ trong nhiều trường hợp
Trong firmware bare-metal đơn giản, thường chỉ có hai context cạnh tranh:
main loop / foreground code
ISR / DMA callback
Khi cập nhật head, tail, dma_busy, ta cần bảo vệ bằng critical section rất ngắn:
uint32_t primask = __get_PRIMASK();
__disable_irq();
// update head, tail, dma_busy, dma_tx_len
__set_PRIMASK(primask);
Nguyên tắc là: disable interrupt càng ngắn càng tốt. Đừng format chuỗi, đừng copy cả kilobyte, đừng gọi HAL dài trong vùng critical section nếu không cần.
7.2 RTOS task: dùng Mutex để giữ nguyên message
Trong RTOS, nhiều task có thể log cùng lúc:
Task A: "Sensor value = 10"
Task B: "USB command received"
Nếu không bảo vệ đúng, log có thể bị trộn:
Sensor USB command value received = 10
Task context có thể dùng Mutex để đảm bảo một message được ghi trọn vẹn vào Ring Buffer:
void Log_Printf(const char *format, ...)
{
char temp[256];
va_list args;
va_start(args, format);
int len = vsnprintf(temp, sizeof(temp), format, args);
va_end(args);
if (len <= 0) {
return;
}
if (len >= (int)sizeof(temp)) {
len = sizeof(temp) - 1;
}
if (osMutexAcquire(logMutexHandle, osWaitForever) == osOK) {
rb_write_drop_message((const uint8_t *)temp, (uint32_t)len);
osMutexRelease(logMutexHandle);
}
logger_kick_tx();
}
7.3 ISR: không Mutex, không format nặng, không dump dài
ISR không nên block. Vì vậy ISR không dùng Mutex. ISR cũng không nên gọi printf() format phức tạp, vì vsnprintf() có thể nặng, khó đoán thời gian, và có thể kéo thêm dependency thư viện C.
Thiết kế thực dụng hơn là cho ISR một API riêng, chỉ nhận raw string ngắn hoặc event code:
void Log_WriteFromISR(const char *msg)
{
/* msg nên ngắn, cố định, không format phức tạp */
rb_write_from_isr((const uint8_t *)msg, strlen(msg));
logger_kick_tx();
}
Nếu ISR cần log dữ liệu động, thường nên đẩy event vào queue rồi để task log sau. ISR mà vừa xử lý timing-critical vừa dump dữ liệu dài thì khác gì bắt lính cứu hỏa vừa chữa cháy vừa viết báo cáo 10 trang.
Quy tắc sống còn
Task có thể chờ trong phạm vi kiểm soát. ISR thì không.
Vì vậy hãy tách API rõ ràng: Log_Printf() cho task, Log_WriteFromISR() hoặc event queue cho ISR.
Đọc thêm: Vì sao ISR không được dùng Mutex trong FreeRTOS?.
7.4 Bảng chọn API theo mô hình firmware
| Mô hình | API nên dùng | Bảo vệ dữ liệu | Điều cần tránh |
|---|---|---|---|
while(1) đơn giản | Log_Printf() | PRIMASK ngắn | log quá dày trong loop nhanh |
| Timer/EXTI ISR | Log_WriteFromISR() hoặc event queue | ISR critical section | printf() format dài trong ISR |
| FreeRTOS task | Log_Printf() | Mutex + critical section ngắn | nhiều task ghi thẳng vào UART |
| DMA callback | internal only | cập nhật state tối thiểu | format log trong callback |
8. Buffer đầy thì làm gì? Đây mới là câu hỏi của sản phẩm thật
Dù thiết kế tốt đến đâu, Ring Buffer vẫn có thể đầy:
- log sinh ra nhanh hơn UART truyền;
- baudrate quá thấp;
- task spam log trong vòng lặp;
- ISR log quá nhiều;
- dump raw data lớn;
- terminal hoặc USB-UART không theo kịp.
Khi buffer đầy, bạn có vài lựa chọn.
8.1 Drop byte: đơn giản nhưng log có thể vỡ chữ
if (next == tail) {
dropped_bytes++;
return false;
}
Ưu điểm:
- đơn giản;
- không block CPU;
- phù hợp real-time cứng;
- hệ thống chính vẫn chạy.
Nhược điểm:
- message có thể bị cắt giữa chừng;
- log khó đọc;
- parser log có thể bị lệch.
8.2 Drop message: mất một dòng nhưng giữ log sạch
Policy này kiểm tra đủ chỗ trước khi ghi. Không đủ thì bỏ cả message:
free_space >= message_len → ghi nguyên message
free_space < message_len → drop nguyên message
Ưu điểm:
- log không vỡ câu;
- dễ đọc;
- dễ phân tích bằng script;
- hợp với debug firmware thực tế.
Nhược điểm:
- message dài dễ bị drop;
- cần counter để biết đã mất bao nhiêu log.
Đây là policy tôi thường chọn cho bản logger cơ bản: mất một dòng còn dễ hiểu hơn mất nửa câu.
8.3 Block chờ UART: chỉ nên dùng như biện pháp debug tạm thời
Block để chờ buffer trống nghe có vẻ “không mất log”, nhưng nó kéo ta quay lại vấn đề ban đầu: log phá timing.
Trong RTOS, block sai chỗ còn có thể gây deadlock hoặc priority inversion. Trong ISR thì càng không nên.
8.4 Double buffer, priority log và binary log: để project xử lý sâu hơn
Khi sản phẩm lớn hơn, bạn có thể nâng cấp:
| Policy nâng cao | Ý tưởng | Khi dùng |
|---|---|---|
| Double buffer | buffer A DMA gửi, buffer B CPU ghi | dump dữ liệu lớn |
| Multi-chunk DMA | gửi 32/64/128 byte mỗi lần | giảm số lần interrupt DMA |
| Priority log | drop DEBUG, giữ ERROR | thiết bị chạy lâu dài |
| Binary log | ghi event ID + data thay vì text | cần tốc độ cao, log nặng |
| Runtime log level | đổi mức log khi đang chạy | field debug / QA |
Những phần này nên để ở project hoặc bài nâng cao, vì nếu nhét hết vào blog này thì bài sẽ biến thành datasheet tự chế. Xem project: STM32 Non-Blocking UART Logger.
9. Production build: logger tốt phải biết biến mất
Một logger tốt không chỉ log đúng trong debug build. Nó còn phải biến mất sạch trong production build.
Không nên xóa tay từng dòng printf(). Cách đó vừa mệt, vừa dễ xóa nhầm, vừa làm mất dấu vết debug cần dùng lại sau này.
Cách tốt hơn là dùng macro:
#ifdef ENABLE_DEBUG_LOG
void Log_PrintfImpl(const char *format, ...);
void Log_WriteFromISRImpl(const char *msg);
#define LOG_PRINTF(...) Log_PrintfImpl(__VA_ARGS__)
#define LOG_FROM_ISR(msg) Log_WriteFromISRImpl(msg)
#else
#define LOG_PRINTF(...) ((void)0)
#define LOG_FROM_ISR(msg) ((void)0)
#endif
Khi ENABLE_DEBUG_LOG không được define:
LOG_PRINTF("value=%d\n", value);
sẽ biến thành:
((void)0);
Mục tiêu là:
- không gọi
vsnprintf(); - không push Ring Buffer;
- không start DMA;
- không giữ log string trong
.rodatanếu compiler tối ưu được; - không kéo logger vào binary nếu không còn tham chiếu.
Kết hợp với GCC/linker options:
Compiler:
-ffunction-sections
-fdata-sections
Linker:
-Wl,--gc-sections
Sau đó kiểm tra bằng file .map, arm-none-eabi-size, hoặc arm-none-eabi-objdump.
Đọc thêm:
10. Checklist thiết kế UART logger cho STM32
Dán checklist này vào đầu project có lẽ sẽ không thừa đâu.
Kiến trúc
Timing
RTOS
Buffer full
Production
11. Từ ý tưởng đến firmware chạy thật
Một kiến trúc đẹp trên giấy chưa chắc đã sống sót khi đưa vào firmware thật.
Với UART logger cũng vậy. Ý tưởng chính nghe có vẻ đơn giản: printf() ghi dữ liệu vào _write(), _write() đẩy log vào Ring Buffer, rồi UART TX DMA lấy từng phần dữ liệu trong buffer để truyền ra ngoài. Khi DMA truyền xong, callback tiếp tục kiểm tra xem trong buffer còn dữ liệu mới hay không.
Nhưng khi bắt tay vào code, ta sẽ gặp ngay những câu hỏi rất thực tế:
- UART và DMA nên cấu hình thế nào trong CubeMX?
- Buffer nên lớn bao nhiêu là vừa?
- Nếu log đến nhanh hơn tốc độ UART truyền thì xử lý ra sao?
- Callback nên viết thế nào để không làm mất log?
- Trong FreeRTOS, task và ISR nên giao tiếp với logger như thế nào?
- Làm sao để debug log biến mất hoàn toàn trong production build?
Đó là phần mình để trong project thực hành, vì nó cần source code đầy đủ, cấu trúc thư mục, cấu hình CubeMX và các bài test cụ thể.
Xem tiếp project: STM32 Non-Blocking UART Logger
Nói ngắn gọn: blog này giúp bạn hiểu thiết kế, còn project giúp bạn mang thiết kế đó xuống board STM32 thật.
12. Tài liệu và note liên quan
Notes trong hệ thống này
- Redirect printf bằng _write() trên STM32 là gì?
- HAL_UART_Transmit() blocking như thế nào?
- Cách tính thời gian truyền UART từ baudrate
- HAL_BUSY xảy ra khi nào trong UART DMA?
- Ring Buffer là gì trong firmware embedded?
- DMA Normal mode và Circular mode khác nhau thế nào?
- Vì sao ISR không được dùng Mutex trong FreeRTOS?
- Cách tắt debug log trong production build
- Dead Code Elimination là gì trong embedded C?
Project thực hành
Tham khảo kỹ thuật bên ngoài
- STMicroelectronics - HAL UART How to Use
- STMicroelectronics - HAL UART exported functions
- FreeRTOS - xSemaphoreCreateMutex
- Memfault Interrupt - Bootstrapping libc with Newlib
- Newlib homepage
13. Kết luận: log là một phần của kiến trúc firmware
Khi mới học STM32, ta có thể nghĩ UART log là chuyện phụ.
Redirect printf() ra UART được là vui rồi.
Nhưng càng làm firmware thật, càng thấy log không phụ chút nào.
Log có thể giúp bạn bắt một bug mất nhiều ngày. Nhưng log cũng có thể tạo ra bug mới nếu nó block CPU, làm lệch timing, làm ISR chạy lâu, làm DMA trả HAL_BUSY, hoặc làm RTOS task chờ sai chỗ.
Một UART logger tốt cần trả lời được các câu hỏi:
Log đi qua đường nào?
Nếu UART đang bận thì log nằm ở đâu?
Nếu buffer đầy thì bỏ cái gì?
Task và ISR có dùng chung API không?
Callback cập nhật state có đúng không?
Production build có còn giữ log không?
Nếu bạn trả lời được những câu hỏi đó, bạn không chỉ đang “in chữ ra terminal”. Bạn đang thiết kế một phần nhỏ nhưng rất quan trọng của hệ thống firmware.
Và đôi khi, chính những phần nhỏ như thế mới phân biệt một project demo chạy được với một firmware đủ tin cậy để ship ra ngoài.
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.
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.
FreeRTOS trên STM32: Task, Queue, Signal và cách tổ chức firmware cho cả team
Triển khai dự án FreeRTOS trên STM32.
USB Device STM32 nâng cao: Vendor Request, Bulk, DMA và Debug với JTAG
USB Device STM32: xử lý vendor request và truyền qua DMA cho bulk và interrupt.
Đọc thêm về BIOS/UEFI
Khám phá các bài viết về BIOS/UEFI, embedded firmware, debugging và system-level thinking.