Ring Buffer trong Embedded C là gì?
Giải thích Ring Buffer trong Embedded C: head/tail, full/empty, ISR producer, task consumer, DMA circular, overflow và debug mất byte.
Bạn nhận UART từng byte trong interrupt. Lúc ít dữ liệu thì ổn, nhưng khi burst dài, byte bị mất. Bạn thử dùng DMA circular nhưng lại không biết byte mới nằm ở đâu. Bạn thêm queue thì tốn RAM hoặc copy quá nhiều. Một cấu trúc rất hay dùng trong embedded để xử lý kiểu này là Ring Buffer.
Nói ngắn gọn: Ring Buffer là buffer vòng dùng hai chỉ số head và tail để lưu stream dữ liệu theo kiểu producer-consumer. Producer ghi vào head, consumer đọc từ tail. Khi tới cuối buffer, chỉ số quay lại đầu. Ring buffer rất phù hợp cho UART RX/TX, logging, DMA stream, ISR-to-task handoff và các pipeline dữ liệu liên tục.
Ring Buffer nằm ở đâu trong firmware?
Producer side
├─ UART RX ISR
├─ DMA circular parser
├─ Sensor task
├─ Logging macro
└─ Protocol driver
Ring Buffer
├─ data[]
├─ head/write index
├─ tail/read index
├─ capacity
├─ count or one-empty-slot rule
└─ overflow counter
Consumer side
├─ Parser task
├─ UART TX backend
├─ Log task
├─ Application state machine
└─ Storage/flush task Ring buffer giúp tách tốc độ producer và consumer. ISR có thể push nhanh, task xử lý sau.
Flow cơ bản
Initialize head and tail
head = tail = 0, buffer đang empty.
Producer writes data at head
Nếu buffer chưa full, ghi byte/record vào data[head].
Advance head
head = next(head), nếu tới cuối thì quay về 0.
Consumer reads data at tail
Nếu buffer chưa empty, đọc data[tail].
Advance tail
tail = next(tail), nếu tới cuối thì quay về 0.
Handle full buffer
Nếu producer push khi full, drop/overwrite/block theo policy.
Head, tail, full, empty
| Khái niệm | Ý nghĩa | Bug nếu sai |
|---|---|---|
| head | Vị trí producer sẽ ghi tiếp | Sai head làm overwrite data |
| tail | Vị trí consumer sẽ đọc tiếp | Sai tail làm đọc lặp hoặc bỏ byte |
| empty | Không có data để đọc | Nhầm empty làm bỏ data |
| full | Không còn chỗ ghi | Nhầm full làm ghi đè |
| capacity | Kích thước mảng data | Quá nhỏ dễ overflow |
| used count | Số phần tử đang có | Sai count gây full/empty lỗi |
| free count | Số chỗ còn trống | Producer không biết backpressure |
| wrap | Chỉ số quay về 0 khi tới cuối | Frame split qua cuối buffer bị parse sai |
Hai cách phân biệt full và empty
Vấn đề kinh điển: nếu chỉ nhìn head == tail, nó có thể là empty hoặc full. Có vài cách xử lý.
| Cách | Ý tưởng | Đánh đổi |
|---|---|---|
| One-empty-slot | Luôn chừa một ô trống, full khi next(head) == tail | Dung lượng usable = capacity - 1 |
| Count variable | Giữ biến count | Cần update atomic/critical đúng |
| Full flag | Thêm flag full | Logic phức tạp hơn |
| Monotonic counters | head_count/tail_count tăng mãi rồi modulo khi index | Tốt cho power-of-two, cần width đủ |
| Overwrite mode | Full thì ghi đè old data và move tail | Mất data cũ có chủ đích |
| Drop newest | Full thì bỏ data mới | Giữ data cũ |
| Block producer | Producer chờ free | Không dùng trong ISR |
| Backpressure | Báo upstream giảm tốc | Không phải peripheral nào hỗ trợ |
Push và pop pseudo-code
Ví dụ theo one-empty-slot, chỉ để hiểu logic:
static inline uint16_t rb_next(uint16_t index, uint16_t size){ index++; if (index >= size) { index = 0; } return index;}bool rb_push(ring_t *rb, uint8_t byte){ uint16_t next = rb_next(rb->head, rb->size); if (next == rb->tail) { rb->drop_count++; return false; } rb->data[rb->head] = byte; rb->head = next; return true;}bool rb_pop(ring_t *rb, uint8_t *byte){ if (rb->head == rb->tail) { return false; } *byte = rb->data[rb->tail]; rb->tail = rb_next(rb->tail, rb->size); return true;}Power-of-two optimization
Nếu buffer size là lũy thừa của 2, có thể dùng mask thay vì modulo/if.
| Điểm | Ý nghĩa | Lưu ý |
|---|---|---|
| Size = 2^N | Ví dụ 64, 128, 256, 1024 | Dễ mask index |
| Mask | index & (size - 1) | Chỉ đúng khi size là power-of-two |
| Nhanh | Tránh phép modulo đắt | Compiler đôi khi tối ưu được |
| Memory | Có thể hơi dư RAM | Đổi lấy tốc độ/đơn giản |
| Monotonic index | head/tail tăng, index = counter & mask | Overflow counter width cần đủ |
| Used count | head - tail nếu unsigned | Cần hiểu wrap integer |
| Debug | Index vật lý khác counter logic | Log cả counter và masked index |
| Portability | Dễ dùng cho UART/logging | Không lạm dụng nếu size arbitrary |
ISR producer, task consumer
Đây là pattern rất phổ biến: ISR nhận byte nhanh, task parse chậm hơn.
UART RX interrupt
UART nhận byte hoặc IDLE event.
ISR reads byte/status
Đọc data register, clear flag cần thiết.
Push byte to ring
Nếu ring full thì drop và tăng counter.
Notify parser task
Dùng task notification/semaphore FromISR nếu có RTOS.
Parser task pops bytes
Task đọc ring buffer và parse frame.
Application handles frame
Xử lý command, response, state machine.
Single producer single consumer
Ring buffer dễ an toàn nhất khi chỉ có một producer và một consumer.
| Pattern | Độ khó | Khuyến nghị |
|---|---|---|
| Single producer, single consumer | Dễ nhất | Phù hợp ISR RX -> task parser |
| Single producer, multi consumer | Khó hơn | Tránh hoặc thêm lock/ownership |
| Multi producer, single consumer | Cần lock hoặc queue | Logging nhiều task nên cẩn thận |
| Multi producer, multi consumer | Phức tạp | Dùng RTOS queue hoặc mutex |
| ISR producer | Không được block | Push nhanh, drop nếu full |
| Task producer | Có thể dùng mutex/critical | Tùy latency |
| DMA producer | Head do hardware counter | Cần snapshot NDTR đúng |
| Lock-free | Chỉ an toàn nếu điều kiện rõ | Không tự tin thì dùng critical section ngắn |
Critical section và volatile
volatile không phải lock. Nó chỉ ngăn compiler tối ưu access theo một số cách. Với shared index giữa ISR và task, cần xem biến có atomic theo kích thước CPU không và có cần critical section không.
| Vấn đề | volatile giúp? | Cần thêm gì |
|---|---|---|
| ISR update head, task đọc head | Giúp task đọc lại từ memory | Có thể cần critical khi đọc nhiều field |
| Task update tail, ISR đọc tail | Giúp ISR thấy tail mới | Update phải atomic |
| uint8_t/uint16_t index trên Cortex-M | Thường atomic nếu aligned | Vẫn cần cẩn thận với count |
| count variable update ở cả hai bên | volatile không đủ | Critical section hoặc tránh count |
| Multi-byte struct record | volatile không đủ | Commit flag hoặc write order |
| DMA buffer | volatile không giải quyết cache | Cache maintenance nếu có D-cache |
| Compiler reorder | volatile hạn chế access object | Memory barrier nếu cần |
| RTOS multi-task | volatile không thay mutex | Mutex/queue/critical tùy pattern |
Ring buffer với DMA circular
DMA circular buffer giống ring buffer, nhưng producer là DMA hardware. Head/write index được tính từ DMA counter.
| Khái niệm | Ý nghĩa | Debug |
|---|---|---|
| DMA buffer | Vùng hardware ghi vòng | Không giống queue hoàn chỉnh |
| NDTR | Số phần tử còn lại DMA sẽ truyền | Dùng để tính write index |
| write index | buffer_size - NDTR | Cần snapshot đúng |
| read index | CPU đã parse tới đâu | Do software quản lý |
| new data | Khoảng read -> write | Có thể wrap |
| IDLE event | Trigger parser khi UART im lặng | Frame boundary hỗ trợ |
| overflow | DMA ghi đè data chưa đọc | Detect bằng distance/high-water |
| cache | Dòng có D-cache cần invalidate | Không do volatile |
Ví dụ ý tưởng:
uint16_t dma_write = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart.hdmarx);process_range(read_index, dma_write);read_index = dma_write;Byte ring hay record ring?
| Tiêu chí | Byte ring | Record ring |
|---|---|---|
| Dữ liệu | Stream byte liên tục | Message/log record cố định hoặc biến đổi |
| UART RX | Rất phù hợp | Cần frame decode trước |
| Logging | Có thể dùng text stream | Tốt cho event log fixed-size |
| Overflow | Mất byte | Mất record |
| Parser | Tìm delimiter/header/length | Pop từng record |
| Memory | Buffer đơn giản | Struct alignment/size |
| Atomic commit | Dễ với byte | Cần đảm bảo record không half-written |
| Debug | Dump bytes | Dump record fields |
Overflow policy
| Policy | Ý nghĩa | Phù hợp |
|---|---|---|
| Drop newest | Full thì bỏ dữ liệu mới | Giữ nguyên data cũ để parser xử lý |
| Drop oldest | Bỏ data cũ để ghi data mới | Log trạng thái mới quan trọng hơn |
| Overwrite | Producer luôn ghi và move tail | Stream realtime chấp nhận mất cũ |
| Block | Producer chờ free | Chỉ task context, không ISR |
| Backpressure | Báo upstream dừng/gửi chậm | Protocol có flow control |
| Reset parser | Overflow thì discard frame hiện tại | Protocol framed |
| Emergency reserve | Giữ chỗ cho ERROR log | Production logging |
| Count drop | Tăng counter | Bắt buộc để debug |
Debug Diary: UART mất byte khi burst
Triệu chứng:
UART RX bình thường OK
Burst dài thì protocol parser báo CRC lỗi
Checklist UART ring buffer mất byte
Debug Diary: Đọc lặp data cũ
Triệu chứng:
Parser đọc lại byte đã xử lý
Frame bị duplicate
Checklist đọc lặp
Debug Diary: Buffer full dù consumer đang chạy
Triệu chứng:
drop_count tăng
Nhưng parser task vẫn chạy đều
Checklist buffer full
Debug Diary: Race condition head/tail
Triệu chứng:
Lỗi rất hiếm
head/tail thỉnh thoảng thành giá trị lạ hoặc parser crash
Checklist race head/tail
Anti-pattern Ring Buffer
| Anti-pattern | Vì sao sai | Cách sửa |
|---|---|---|
| Không có full check | Ghi đè data chưa đọc | Full condition rõ |
| Không có drop counter | Mất data im lặng | drop_count/high-water |
| Dùng count shared không lock | Race giữa ISR/task | One-empty-slot hoặc critical |
| Parse không xử lý wrap | Frame split bị lỗi | Range processing wrap-aware |
| Multi-producer không lock | Head corruption | Mutex/critical/queue |
| Producer block trong ISR | ISR không được block | Drop hoặc notify |
| Buffer size quá nhỏ | Burst là mất | Tính theo data rate và latency |
| Tin volatile là đủ | Không bảo vệ compound operation | Atomic/critical đúng chỗ |
Instrumentation nên có
| Metric | Ý nghĩa | Debug |
|---|---|---|
| push_count | Số phần tử producer push | Input rate |
| pop_count | Số phần tử consumer pop | Output rate |
| drop_count | Số phần tử bị drop | Overflow |
| max_used | Mức sử dụng cao nhất | Sizing buffer |
| current_used | Dữ liệu đang chờ | Runtime status |
| overflow_time | Timestamp lần overflow gần nhất | Tương quan lỗi |
| head/tail snapshot | State khi lỗi | Debug race/wrap |
| producer_context | ISR/task/module | Ai ghi nhiều |
| consumer_latency | Thời gian giữa notify và pop | RTOS scheduling |
| assert indices | head/tail < size | Detect corruption sớm |
Ring Buffer debug playbook
Ring Buffer debug playbook
Reliability checklist
Checklist ring buffer ổn định
Khi đọc log ring buffer nên tìm gì?
| Cần hiểu | Tìm trong log | Câu hỏi |
|---|---|---|
| Rate | push_count/pop_count | Producer có nhanh hơn consumer không? |
| Overflow | drop_count | Có mất data không? |
| Usage | max_used/current_used | Buffer đủ không? |
| State | head/tail | Có wrap đúng không? |
| Context | producer source | Ai spam buffer? |
| Latency | consumer wake/process delay | Task có bị starve không? |
| UART errors | ORE/FE/NE | Mất byte trước khi vào buffer không? |
| Parser | frame boundary | Frame split qua wrap không? |
| DMA | NDTR/write index/read index | DMA circular tính đúng không? |
| Corruption | assert index range | Có memory overwrite không? |
Câu hỏi tự kiểm tra
Tự kiểm tra sau khi đọc note này
Blog seeds
- Ring Buffer trong Embedded C là gì?
- Head tail trong circular buffer giải thích dễ hiểu
- UART RX ISR dùng ring buffer thế nào?
- DMA circular buffer và ring buffer khác nhau ra sao?
- Debug mất byte do ring buffer overflow
Bài liên quan
- DMA Normal vs Circular Mode trong STM32
- Producer Consumer Logging trong STM32
- HAL_BUSY UART DMA là gì?
- FreeRTOS ISR Mutex là gì?
- STM32 printf write redirect
- UART Transmit Blocking là gì?
- UART Baudrate Time Calculation
- Production Debug Log Macro
- GCC Dead Code Elimination
- Startup Code STM32
Nguồn tham khảo public
- FreeRTOS documentation
- STM32Cube MCU Packages
- STM32CubeG0
- Arm Cortex-M documentation
- CMSIS documentation
Bài viết này hữu ích với bạn?
Chia sẻ, góp ý, hoặc ủng hộ nếu bạn thấy nội dung này có giá trị.
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.
USB CDC ACM là gì?
CDC ACM trên STM32: tại sao giống UART về trải nghiệm nhưng khác về implementation, SET_CONTROL_LINE_STATE, DTR và điểm dễ nhầm khi dùng với terminal.
CubeMX USB pitfalls thường gặp
Những điểm CubeMX không làm đúng khi generate USB code: HID mouse descriptor thay vì keyboard, NVIC không bật, Sof_enable DISABLE, USBD_static_malloc sai kích thước.
USB Bulk Transfer cho debug dump là gì?
Bulk IN cho RAM dump trên STM32G0: state machine chunk 64 byte, DataIn callback pipeline, tại sao không dùng ZLP, Zadig cho Windows và throughput thực đo.
Biến note thành bài viết hoàn chỉnh
Notes là nơi ghi nhanh khái niệm.