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])
}