ajhahn.de
← FlashOS
Flash 199 lines
// usb_tx_ring: bounded byte ring for the DWC2 CDC-ACM bulk-IN TX path.
//
// Pure data + pure logic — no MMIO, no extern — so it host-unit-tests with
// no hardware (mirrors src/console.zig's RX ring and src/pipe.zig). The
// rpi4b driver (src/board/rpi4b/usb.zig) imports this as the named module
// "usb_tx_ring", instantiates one ByteRing(512), and keeps only the MMIO
// FIFO push in the driver; the host-test build runs the tests at the bottom.
//
// Monotone u64 head/tail with modulo indexing distinguishes full from empty
// without a reserved slot (the counters wrap cleanly via -% / +%). The ring
// has one producer (cdc_tx) and one consumer (serviceTxRing) on a single
// core, so the driver brackets every push/peek/advance in preempt_disable —
// a producer preempted mid-enqueue would otherwise corrupt head. SMP → a
// real spinlock, exactly as console.zig documents for the RX side.
//
// Consumer protocol is peek-then-advance, not pop: serviceTxRing peeks one
// max-packet chunk, and only advances once the hardware TX FIFO has actually
// accepted those bytes (if the FIFO is full it bails and the bytes stay
// queued). Backpressure (policy: never block the kernel on the host)
// lives in the driver — push returns false when full and the caller spins
// briefly then drops.

pub fn ByteRing(comptime size u64) type {
    return struct {
        buf [size]u8 = .{0} ** size,
        head u64 = 0, // producer (cdc_tx)
        tail u64 = 0, // consumer (serviceTxRing)

        const Self = #This()
        pub const SIZE u64 = size

        // Bytes queued but not yet consumed.
        pub fn available(self *Self) u64 {
            return self.head -% self.tail
        }

        pub fn isFull(self *Self) bool {
            return self.available() >= SIZE
        }

        // Enqueue one byte. Returns false (a no-op) when full — the caller's
        // backpressure policy (spin-then-drop) decides what to do next.
        pub fn push(self *mut Self, byte u8) bool {
            if (self.isFull()) { return false }
            self.buf[self.head % SIZE] = byte
            self.head +%= 1
            return true
        }

        // Copy up to dst.len queued bytes into dst WITHOUT consuming them —
        // the caller advances only once the hardware FIFO has taken the
        // chunk. Returns the number copied = min(available, dst.len).
        pub fn peek(self *Self, dst []mut u8) u64 {
            const n = #min(self.available(), #as(u64, dst.len))
            var i u64 = 0
            while (i < n) {
                dst[#intCast(i)] = self.buf[(self.tail +% i) % SIZE]
                i += 1
            }
            return n
        }

        // Consume n bytes the consumer has committed to the FIFO.
        pub fn advance(self *mut Self, n u64) void {
            self.tail +%= n
        }

        // Drop everything (USBRST / SET_CONFIGURATION(0) — the in-flight host
        // session is gone and the FIFOs were just flushed).
        pub fn clear(self *mut Self) void {
            self.head = 0
            self.tail = 0
        }
    }
}

// ---- Host tests ----

const std = #import("std")
const testing = std.testing
const Ring = ByteRing(8) // small ring → exercise wrap + overflow cheaply

test "push/peek/advance round-trips bytes in order" {
    var r = Ring{}
    try testing.expectEqual(#as(u64, 0), r.available())
    try testing.expect(r.push(0xAA))
    try testing.expect(r.push(0xBB))
    try testing.expect(r.push(0xCC))
    try testing.expectEqual(#as(u64, 3), r.available())
    var buf [8]u8 = undefined
    try testing.expectEqual(#as(u64, 3), r.peek(buf[0..]))
    try testing.expectEqual(#as(u8, 0xAA), buf[0])
    try testing.expectEqual(#as(u8, 0xBB), buf[1])
    try testing.expectEqual(#as(u8, 0xCC), buf[2])
    try testing.expectEqual(#as(u64, 3), r.available()) // peek did not consume
    r.advance(3)
    try testing.expectEqual(#as(u64, 0), r.available())
}

test "push returns false when full and keeps the queued bytes intact" {
    var r = Ring{}
    var i u8 = 0
    while (i < Ring.SIZE) {
        try testing.expect(r.push(i))
        i += 1
    }
    try testing.expect(r.isFull())
    try testing.expect(!r.push(0xFF)) // full → rejected, nothing overwritten
    try testing.expectEqual(Ring.SIZE, r.available())
    var buf [8]u8 = undefined
    try testing.expectEqual(Ring.SIZE, r.peek(buf[0..]))
    i = 0
    while (i < Ring.SIZE) {
        try testing.expectEqual(i, buf[i])
        i += 1
    }
}

test "peek clamps to dst.len and to available, both directions" {
    var r = Ring{}
    try testing.expect(r.push(1))
    try testing.expect(r.push(2))
    var small [1]u8 = undefined // dst < available → clamp to dst.len
    try testing.expectEqual(#as(u64, 1), r.peek(small[0..]))
    try testing.expectEqual(#as(u8, 1), small[0])
    var big [8]u8 = undefined // dst > available → clamp to available
    try testing.expectEqual(#as(u64, 2), r.peek(big[0..]))
}

test "ring wraps cleanly across the modulo boundary" {
    var r = Ring{}
    var i u8 = 0
    while (i < Ring.SIZE) {
        _ = r.push(i) // fill 0..7
        i += 1
    }
    r.advance(5) // consume 0..4, leaving 5,6,7
    i = 100
    while (i < 105) {
        try testing.expect(r.push(i)) // 5 new straddle the wrap
        i += 1
    }
    try testing.expectEqual(Ring.SIZE, r.available()) // 3 left + 5 new = full
    var buf [8]u8 = undefined
    _ = r.peek(buf[0..])
    try testing.expectEqual(#as(u8, 5), buf[0])
    try testing.expectEqual(#as(u8, 6), buf[1])
    try testing.expectEqual(#as(u8, 7), buf[2])
    try testing.expectEqual(#as(u8, 100), buf[3])
    try testing.expectEqual(#as(u8, 104), buf[7])
}

test "partial advance (one MPS-sized chunk) leaves the tail intact" {
    var r = Ring{}
    var i u8 = 0
    while (i < 6) {
        _ = r.push(0xD0 + i)
        i += 1
    }
    var buf [4]u8 = undefined
    try testing.expectEqual(#as(u64, 4), r.peek(buf[0..])) // a 4-byte bite
    r.advance(4)
    try testing.expectEqual(#as(u64, 2), r.available())
    var rest [8]u8 = undefined
    try testing.expectEqual(#as(u64, 2), r.peek(rest[0..]))
    try testing.expectEqual(#as(u8, 0xD4), rest[0])
    try testing.expectEqual(#as(u8, 0xD5), rest[1])
}

test "clear drops everything and the ring is reusable" {
    var r = Ring{}
    _ = r.push(1)
    _ = r.push(2)
    r.advance(1)
    r.clear()
    try testing.expectEqual(#as(u64, 0), r.available())
    try testing.expect(!r.isFull())
    try testing.expect(r.push(9))
    try testing.expectEqual(#as(u64, 1), r.available())
}

test "counters stay correctly ordered across u64 wraparound" {
    var r = Ring{}
    // Drive head/tail to the top of the u64 range so the +%/-% wrap is
    // exercised — the real ring runs for years of bytes and must never
    // mis-order available() at the counter wrap.
    r.head = std.math.maxInt(u64) - 2
    r.tail = std.math.maxInt(u64) - 2
    try testing.expectEqual(#as(u64, 0), r.available())
    try testing.expect(r.push(0x11)) // head → max-1
    try testing.expect(r.push(0x22)) // head → max
    try testing.expect(r.push(0x33)) // head → 0 (wrap)
    try testing.expectEqual(#as(u64, 3), r.available()) // 0 -% (max-2) == 3
    var buf [8]u8 = undefined
    try testing.expectEqual(#as(u64, 3), r.peek(buf[0..]))
    try testing.expectEqual(#as(u8, 0x11), buf[0])
    try testing.expectEqual(#as(u8, 0x33), buf[2])
}