bbqueue

repository·main·Indexed 19 days ago

https://github.com/jamesmunns/bbqueue

A Single Producer Single Consumer (SPSC), lockless, no_std, thread-safe queue based on BipBuffers. Optimized for embedded systems and DMA transfers, it provides contiguous memory blocks to avoid wrap-around logic mid-transfer. It supports both stream-based and framed data interfaces, with customizable configurations for storage, concurrency, atomicity, and ownership via pre-configured nicknames like Churrasco.

Tokens
13.9K
Snippets
44
Records
61
Agent score
67%

What's inside bbqueue

  1. What is BBQueue and when should I use it?

    main

    BBQueue (BipBuffer Queue) is a Single Producer Single Consumer (SPSC), lockless, no_std, thread-safe queue based on the BipBuffer design.

    It is specifically designed for use with DMA (Direct Memory Access) on embedded systems. Unlike standard Circular/Ring Buffers where data must be pushed one piece at a time, BBQueue grants the producer a block of contiguous memory. This allows a DMA engine to fill or empty the buffer efficiently without needing to handle wrap-around logic mid-transfer.

  2. Understand BBQueue nicknames and customization options

    main

    BBQueue uses generics (referred to as nicknames) to allow developers to customize the queue's behavior across four dimensions:

    1. Storage: Whether byte storage is inline (using const-generics) or heap-allocated.
    2. Concurrency Model: Whether the queue is polling-only or supports async/await for sending/receiving.
    3. Atomicity: Whether the queue uses a lock-free algorithm with CAS (Compare-And-Swap) atomics, or uses a critical section (for targets lacking CAS atomics).
    4. Ownership: Whether the queue is reference counted, allowing Producer and Consumer halves to be passed around without lifetime constraints.

    See the nicknames module in the crate for the sixteen available variants.

  3. How to use BBQueue for local usage

    main

    To use BBQueue locally, you can use one of the pre-configured variants (nicknames) like Churrasco. The workflow involves:

    1. Creating the buffer.
    2. Obtaining a stream_producer and a stream_consumer.
    3. Writing: Using grant_exact(n) to get a write grant, filling it, and then calling commit(n) to make the data available.
    4. Reading: Using read() to get a read grant, accessing the data, and then calling release(n) to free the space.
    use bbqueue::nicknames::Churrasco;
    
    // Create a buffer with six elements
    let bb: Churrasco<6> = Churrasco::new();
    let prod = bb.stream_producer();
    let cons = bb.stream_consumer();
    
    // Request space for one byte
    let mut wgr = prod.grant_exact(1).unwrap();
    
    // Set the data
    wgr[0] = 123;
    
    assert_eq!(wgr.len(), 1);
    
    // Make the data ready for consuming
    wgr.commit(1);
    
    // Read all available bytes
    let rgr = cons.read().unwrap();
    
    assert_eq!(rgr[0], 123);
    
    // Release the space for later writes
    rgr.release(1);
  4. What is BBQueue and how does it work?

    main

    BBQueue (BipBuffer Queue) is a Single Producer Single Consumer (SPSC), lockless, no_std, thread-safe queue based on the BipBuffer design.

    Unlike standard Circular/Ring Buffers that require data to be pushed one piece at a time, BBQueue grants access to contiguous blocks of memory. This makes it specifically designed for use with DMA (Direct Memory Access) on embedded systems, as a DMA engine can fill or empty a single contiguous block without needing to handle wrap-around logic mid-transfer.

    Key characteristics:

    • SPSC: One producer and one consumer.
    • Lockless: Uses atomic operations for thread safety.
    • Contiguous Grants: Provides slices of memory for efficient bulk transfers.
    # use bbqueue::BBBuffer;
    #
    // Create a buffer with six elements
    let bb: BBBuffer<6> = BBBuffer::new();
    let (mut prod, mut cons) = bb.try_split().unwrap();
    
    // Request space for one byte
    let mut wgr = prod.grant_exact(1).unwrap();
    
    // Set the data
    wgr[0] = 123;
    
    assert_eq!(wgr.len(), 1);
    
    // Make the data ready for consuming
    wgr.commit(1);
    
    // Read all available bytes
    let rgr = cons.read().unwrap();
    
    assert_eq!(rgr[0], 123);
    
    // Release the space for later writes
    rgr.release(1);
  5. How StreamGrantW and StreamGrantR work

    main

    Grants are temporary handles to the underlying buffer memory that implement Deref<Target = [u8]> and DerefMut.

    StreamGrantW (Writing Grant)

    • Purpose: Provides mutable access to a slice of the buffer for writing.
    • Lifecycle: You must call .commit(used) to finalize the write and notify the consumer.
    • Safety: It is marked #[must_use] because failing to commit means the data written is not visible to the consumer.

    StreamGrantR (Reading Grant)

    • Purpose: Provides access to a slice of the buffer for reading. It also implements DerefMut so you can mutate the storage in-place (e.g., for in-place decoding).
    • Lifecycle: You must call .release(used) to free the space back to the producer.

    Both types implement Drop. If they are dropped without an explicit commit or release, they will attempt to commit/release based on their internal tracking, but explicit calls are the intended way to manage the stream flow.

  6. Choose a storage strategy for BBQueue

    main

    BBQueue uses a Storage trait to define how the ring buffer's data is backed in memory. You can choose between two primary strategies depending on your allocation requirements:

    1. Inline Storage (Inline<const N: usize>): Stores data directly within the struct (e.g., as an owned [u8; N] array). This is ideal for static allocation or environments where you want a fixed-size buffer without a heap.
    2. Heap Storage (BoxedSlice): Allocates data on the heap. This is useful when you need dynamically sized storage determined at runtime (e.g., from a configuration file or CLI argument). Note that this requires the alloc feature to be enabled.

    To use these, you provide them as the storage backend when initializing your BBQueue instance.

  7. Use BBQueue nicknames for quick configuration

    main

    Instead of manually configuring the complex generic parameters of BBQueue, you can use the nicknames module to select pre-configured variants.

    For example, Churrasco is a common choice that provides:

    • Inline storage
    • Hardware atomic support
    • No async support
    • No reference counting

    Refer to the bbqueue::nicknames module for the full list of sixteen available variants.

  8. Choose between CAS and Critical Section coordination

    main

    BBQueue uses a Coord trait to arbitrate access between Producers and Consumers. The specific implementation used depends on your hardware capabilities:

    1. CAS (Compare and Swap) Coordination: This is the preferred method for most targets. It is automatically enabled if the target supports atomic pointers (#[cfg(target_has_atomic = "ptr")]).
    2. Critical Section (CS) Coordination: Use this if you are on an embedded target that lacks Compare and Swap atomics (for example, cortex-m0 or thumbv6m). This is enabled via the critical-section feature.

    Note: The Coord trait is marked unsafe because incorrect implementations can lead to Undefined Behavior (UB). End-users should typically use the high-level Producer and Consumer APIs provided by BBQueue rather than implementing Coord directly.

  9. Use CsCoord for coordination on bare metal targets

    main

    If you are targeting bare metal platforms that lack CAS (Compare-And-Swap) atomics (such as cortex-m0 or thumbv6m), use CsCoord for queue coordination.

    CsCoord implements the Coord trait using critical sections. It ensures that coordination operations (obtaining or releasing grants) are performed within a critical section, but the actual data processing within a grant occurs outside the critical section to minimize latency.

  10. How Framed mode works in BBQueue

    main

    Framed mode allows BBQueue to operate with variable-length packets (chunks) rather than a continuous stream of bytes. It achieves this by prepending an internal size header to each frame.

    When you request a grant, the system calculates a header size based on the maximum requested size. This header is used to store the actual number of bytes committed.

    Important Considerations:

    • Header Overhead: You must factor the header size into your total buffer capacity calculations. The header size is determined by the max_sz requested in grant(), not the number of bytes actually committed.
    • Header Size Mapping: | Grant Size (bytes) | Header size (bytes) | | :--- | :--- | | 1..(2^7) | 1 | | (2^7)..(2^14) | 2 | | (2^14)..(2^21) | 3 | | (2^21)..(2^28) | 4 | | (2^28)..(2^35) | 5 | | (2^35)..(2^42) | 6 | | (2^42)..(2^49) | 7 | | (2^49)..(2^56) | 8 | | (2^56)..(2^64) | 9 |

    Lifecycle Rules:

    • Writing: If a FrameGrantW is dropped without calling commit() or to_commit(), no frame is committed.
    • Reading: If a FrameGrantR is dropped without calling release(), no space is released back to the producer.
    use bbqueue::BBBuffer;
    
    let bb: BBBuffer<1000> = BBBuffer::new();
    let (mut prod, mut cons) = bb.try_split_framed().unwrap();
    
    // One frame in, one frame out
    let mut wgrant = prod.grant(128).unwrap();
    assert_eq!(wgrant.len(), 128);
    for (idx, i) in wgrant.iter_mut().enumerate() {
        *i = idx as u8;
    }
    wgrant.commit(128);
    
    let rgrant = cons.read().unwrap();
    assert_eq!(rgrant.len(), 128);
    for (idx, i) in rgrant.iter().enumerate() {
        assert_eq!(*i, idx as u8);
    }
    rgrant.release();
  11. How the BbqHandle trait works

    main

    The BbqHandle trait is an abstraction used to manage how a BBQueue is accessed and shared. It decouples the data storage (where the bytes live) from the header storage (how the queue's state/metadata is shared between producers and consumers).

    By using BbqHandle, you can write generic code that works regardless of whether the BBQueue is stored as a 'static reference (common in embedded systems) or wrapped in an Arc (common in hosted environments).

    Instead of being generic over four different types, most consumer code only needs to be generic over a single type Q: BbqHandle. The trait bundles the following associated types:

    • Target: The type used to reference the BBQueue (must implement Deref<Target = BBQueue<...>> and Clone).
    • Storage: The mechanism for data storage (implements Storage).
    • Coord: The coordination mechanism for producers/consumers (implements Coord).
    • Notifier: The notification mechanism (implements Notifier).
  12. How BBQueue works

    main

    BBQueue (BipBuffer Queue) is a Single Producer Single Consumer (SPSC), lockless, no_std, thread-safe queue based on the BipBuffer design.

    Unlike standard circular/ring buffers where data must be pushed one piece at a time, BBQueue grants the user a block of contiguous memory. This makes it ideal for use with DMA (Direct Memory Access) on embedded systems, as a DMA engine can fill or empty a contiguous buffer without CPU intervention.

    BBQueue uses generics to allow developers to customize four main aspects of the data structure:

    1. Storage: Inline (const-generic) or heap-allocated.
    2. Notification: Polling-only or async/await support.
    3. Coordination: Lock-free algorithm with CAS (Compare-And-Swap) atomics, or critical sections (for targets without CAS).
    4. Ownership: Reference counted (allowing Producer and Consumer halves to be passed around without lifetime constraints) or not.
    use bbqueue::nicknames::Churrasco;
    
    // Create a buffer with six elements
    let bb: Churrasco<6> = Churrasco::new();
    let prod = bb.stream_producer();
    let cons = bb.stream_consumer();
    
    // Request space for one byte
    let mut wgr = prod.grant_exact(1).unwrap();
    
    // Set the data
    wgr[0] = 123;
    
    assert_eq!(wgr.len(), 1);
    
    // Make the data ready for consuming
    wgr.commit(1);
    
    // Read all available bytes
    let rgr = cons.read().unwrap();
    
    assert_eq!(rgr[0], 123);
    
    // Release the space for later writes
    rgr.release(1);