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.

Cập nhật 15 phút đọc
STM32 Debug cover

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ố headtail để 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

01 Init

Initialize head and tail

head = tail = 0, buffer đang empty.

02 Push

Producer writes data at head

Nếu buffer chưa full, ghi byte/record vào data[head].

03 AdvanceH

Advance head

head = next(head), nếu tới cuối thì quay về 0.

04 Pop

Consumer reads data at tail

Nếu buffer chưa empty, đọc data[tail].

05 AdvanceT

Advance tail

tail = next(tail), nếu tới cuối thì quay về 0.

06 Overflow

Handle full buffer

Nếu producer push khi full, drop/overwrite/block theo policy.

Ring buffer hoạt động bằng cách producer ghi head và consumer đọc tail.

Head, tail, full, empty

Ring buffer state
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ý.

Phân biệt full và empty
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.

Power-of-two ring buffer
Đ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.

01 IRQ

UART RX interrupt

UART nhận byte hoặc IDLE event.

02 Read

ISR reads byte/status

Đọc data register, clear flag cần thiết.

03 Push

Push byte to ring

Nếu ring full thì drop và tăng counter.

04 Notify

Notify parser task

Dùng task notification/semaphore FromISR nếu có RTOS.

05 Parse

Parser task pops bytes

Task đọc ring buffer và parse frame.

06 Handle

Application handles frame

Xử lý command, response, state machine.

ISR producer đẩy byte vào ring buffer, task consumer xử lý protocol.

Single producer single consumer

Ring buffer dễ an toàn nhất khi chỉ có một producer và một consumer.

Concurrency patterns
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.

volatile và critical section
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.

DMA circular + ring parser
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?

Byte ring vs 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

Ring buffer 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 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ó

Instrumentation cho Ring Buffer
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ì?

Ring buffer log reading checklist
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

Nguồn tham khảo public

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ị.

Góp ý

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.

Biến note thành bài viết hoàn chỉnh

Notes là nơi ghi nhanh khái niệm.