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.

Cập nhật 20 phút đọc
Sơ đồ kiến trúc UART logger non-blocking trên STM32: application ghi log vào Ring Buffer, DMA truyền ra UART ở background.

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
Kiến trúc UART logger non-blocking trên STM32 dùng Ring Buffer và DMA
Kiến trúc tổng thể: CPU chỉ đưa log vào buffer; DMA chịu trách nhiệm truyền vật lý ra UART.

Trong kiến trúc này:

  • printf() hoặc LOG_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ầnMốc thời gian thường gặp
FreeRTOS tick1 ms
Vòng đọc sensor nhanhvài chục đến vài trăm µs
USB SOF full-speed1 ms
Một số xử lý interruptcần kết thúc càng nhanh càng tốt
Watchdogcó 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
Producer Consumer pattern trong UART logger
Producer tạo log nhanh; UART tiêu thụ chậm; Ring Buffer đứng giữa để hấp thụ chênh lệch tốc độ.

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ầnTrách nhiệm
Logger APINhận message từ application/task/ISR
Format bufferGom format string thành một message hoàn chỉnh
Ring BufferLưu log đang chờ truyền
DMA TX engineStart DMA khi UART rảnh
Tx complete callbackCậ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

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;
}
Ring Buffer khi dữ liệu nằm liên tục từ tail tới head

Case 1: dữ liệu liên tục, DMA có thể gửi một chunk thẳng

Ring Buffer khi dữ liệu bị wrap quanh cuối mảng

Case 2: dữ liệu wrap-around, DMA nên gửi phần tail → end trước

Hai trạng thái quan trọng khi chọn chunk DMA từ Ring Buffer.

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.

So sánh task context và ISR context trong UART logger
Task có thể dùng Mutex; ISR chỉ nên làm việc rất ngắn và không block.

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ìnhAPI nên dùngBảo vệ dữ liệuĐiều cần tránh
while(1) đơn giảnLog_Printf()PRIMASK ngắnlog quá dày trong loop nhanh
Timer/EXTI ISRLog_WriteFromISR() hoặc event queueISR critical sectionprintf() format dài trong ISR
FreeRTOS taskLog_Printf()Mutex + critical section ngắnnhiều task ghi thẳng vào UART
DMA callbackinternal onlycập nhật state tối thiểuformat 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ưởngKhi dùng
Double bufferbuffer A DMA gửi, buffer B CPU ghidump dữ liệu lớn
Multi-chunk DMAgửi 32/64/128 byte mỗi lầngiảm số lần interrupt DMA
Priority logdrop DEBUG, giữ ERRORthiết bị chạy lâu dài
Binary logghi event ID + data thay vì textcần tốc độ cao, log nặng
Runtime log levelđổi mức log khi đang chạyfield 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 .rodata nế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

Project thực hành

Tham khảo kỹ thuật bên ngoài


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.

Đọ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.