copy
This commit is contained in:
293
gateware/channels_to_usb_stream.py
Normal file
293
gateware/channels_to_usb_stream.py
Normal file
@@ -0,0 +1,293 @@
|
||||
from amaranth import *
|
||||
from amaranth.build import Platform
|
||||
from amaranth.lib.fifo import SyncFIFO
|
||||
from amlib.stream import StreamInterface, connect_fifo_to_stream
|
||||
from amlib.test import GatewareTestCase, sync_test_case
|
||||
|
||||
class ChannelsToUSBStream(Elaboratable):
|
||||
def __init__(self, max_nr_channels=2, sample_width=24, max_packet_size=256):
|
||||
assert sample_width in [16, 24, 32]
|
||||
|
||||
# parameters
|
||||
self._max_nr_channels = max_nr_channels
|
||||
self._channel_bits = Shape.cast(range(max_nr_channels)).width
|
||||
self._sample_width = sample_width
|
||||
self._fifo_depth = 2 * max_packet_size
|
||||
|
||||
# ports
|
||||
self.no_channels_in = Signal(self._channel_bits + 1)
|
||||
self.channel_stream_in = StreamInterface(name="channel_stream", payload_width=self._sample_width, extra_fields=[("channel_nr", self._channel_bits)])
|
||||
self.usb_stream_out = StreamInterface(name="usb_stream")
|
||||
self.audio_in_active = Signal()
|
||||
self.data_requested_in = Signal()
|
||||
self.frame_finished_in = Signal()
|
||||
|
||||
# debug signals
|
||||
self.feeder_state = Signal()
|
||||
self.current_channel = Signal(self._channel_bits)
|
||||
self.level = Signal(range(self._fifo_depth + 1))
|
||||
self.fifo_read = Signal()
|
||||
self.fifo_full = Signal()
|
||||
self.fifo_level_insufficient = Signal()
|
||||
self.done = Signal.like(self.level)
|
||||
self.out_channel = Signal(self._channel_bits)
|
||||
self.usb_channel = Signal.like(self.out_channel)
|
||||
self.usb_byte_pos = Signal.like(2)
|
||||
self.skipping = Signal()
|
||||
self.filling = Signal()
|
||||
|
||||
def elaborate(self, platform: Platform) -> Module:
|
||||
m = Module()
|
||||
m.submodules.out_fifo = out_fifo = SyncFIFO(width=8 + self._channel_bits, depth=self._fifo_depth, fwft=True)
|
||||
|
||||
channel_stream = self.channel_stream_in
|
||||
channel_valid = Signal()
|
||||
channel_ready = Signal()
|
||||
|
||||
out_valid = Signal()
|
||||
out_stream_ready = Signal()
|
||||
|
||||
# latch packet start and end
|
||||
first_packet_seen = Signal()
|
||||
frame_finished_seen = Signal()
|
||||
|
||||
with m.If(self.data_requested_in):
|
||||
m.d.sync += [
|
||||
first_packet_seen.eq(1),
|
||||
frame_finished_seen.eq(0),
|
||||
]
|
||||
|
||||
with m.If(self.frame_finished_in):
|
||||
m.d.sync += [
|
||||
frame_finished_seen.eq(1),
|
||||
first_packet_seen.eq(0),
|
||||
]
|
||||
|
||||
m.d.comb += [
|
||||
self.usb_stream_out.payload.eq(out_fifo.r_data[:8]),
|
||||
self.out_channel.eq(out_fifo.r_data[8:]),
|
||||
self.usb_stream_out.valid.eq(out_valid),
|
||||
out_stream_ready.eq(self.usb_stream_out.ready),
|
||||
channel_valid.eq(channel_stream.valid),
|
||||
channel_stream.ready.eq(channel_ready),
|
||||
self.level.eq(out_fifo.r_level),
|
||||
self.fifo_full.eq(self.level >= (self._fifo_depth - 4)),
|
||||
self.fifo_read.eq(out_fifo.r_en),
|
||||
]
|
||||
|
||||
with m.If(self.usb_stream_out.valid & self.usb_stream_out.ready):
|
||||
m.d.sync += self.done.eq(self.done + 1)
|
||||
|
||||
with m.If(self.data_requested_in):
|
||||
m.d.sync += self.done.eq(0)
|
||||
|
||||
current_sample = Signal(32 if self._sample_width > 16 else 16)
|
||||
current_channel = Signal(self._channel_bits)
|
||||
current_byte = Signal(2 if self._sample_width > 16 else 1)
|
||||
|
||||
m.d.comb += self.current_channel.eq(current_channel),
|
||||
|
||||
bytes_per_sample = 4
|
||||
last_byte_of_sample = bytes_per_sample - 1
|
||||
|
||||
# USB audio still sends 32 bit samples,
|
||||
# even if the descriptor says 24
|
||||
shift = 8 if self._sample_width == 24 else 0
|
||||
|
||||
out_fifo_can_write_sample = Signal()
|
||||
m.d.comb += out_fifo_can_write_sample.eq(
|
||||
out_fifo.w_rdy
|
||||
& (out_fifo.w_level < (out_fifo.depth - bytes_per_sample)))
|
||||
|
||||
# this FSM handles writing into the FIFO
|
||||
with m.FSM(name="fifo_feeder") as fsm:
|
||||
m.d.comb += self.feeder_state.eq(fsm.state)
|
||||
|
||||
with m.State("WAIT"):
|
||||
m.d.comb += channel_ready.eq(out_fifo_can_write_sample)
|
||||
|
||||
# discard all channels above no_channels_in
|
||||
# important for stereo operation
|
||||
with m.If( out_fifo_can_write_sample
|
||||
& channel_valid
|
||||
& (channel_stream.channel_nr < self.no_channels_in)):
|
||||
m.d.sync += [
|
||||
current_sample.eq(channel_stream.payload << shift),
|
||||
current_channel.eq(channel_stream.channel_nr),
|
||||
]
|
||||
m.next = "SEND"
|
||||
|
||||
with m.State("SEND"):
|
||||
m.d.comb += [
|
||||
out_fifo.w_data[:8].eq(current_sample[0:8]),
|
||||
out_fifo.w_data[8:].eq(current_channel),
|
||||
out_fifo.w_en.eq(1),
|
||||
]
|
||||
m.d.sync += [
|
||||
current_byte.eq(current_byte + 1),
|
||||
current_sample.eq(current_sample >> 8),
|
||||
]
|
||||
|
||||
with m.If(current_byte == last_byte_of_sample):
|
||||
m.d.sync += current_byte.eq(0)
|
||||
m.next = "WAIT"
|
||||
|
||||
|
||||
channel_counter = Signal.like(self.no_channels_in)
|
||||
byte_pos = Signal(2)
|
||||
first_byte = byte_pos == 0
|
||||
last_byte = byte_pos == 3
|
||||
|
||||
with m.If(out_valid & out_stream_ready):
|
||||
m.d.sync += byte_pos.eq(byte_pos + 1)
|
||||
|
||||
with m.If(last_byte):
|
||||
m.d.sync += channel_counter.eq(channel_counter + 1)
|
||||
with m.If(channel_counter == (self.no_channels_in - 1)):
|
||||
m.d.sync += channel_counter.eq(0)
|
||||
|
||||
with m.If(self.data_requested_in):
|
||||
m.d.sync += channel_counter.eq(0)
|
||||
|
||||
fifo_level_sufficient = Signal()
|
||||
m.d.comb += [
|
||||
self.usb_channel.eq(channel_counter),
|
||||
self.usb_byte_pos.eq(byte_pos),
|
||||
fifo_level_sufficient.eq(out_fifo.level >= (self.no_channels_in << 2)),
|
||||
self.fifo_level_insufficient.eq(~fifo_level_sufficient),
|
||||
]
|
||||
|
||||
with m.If(self.frame_finished_in):
|
||||
m.d.sync += byte_pos.eq(0)
|
||||
|
||||
# this FSM handles reading fron the FIFO
|
||||
# this FSM provides robustness against
|
||||
# short reads. On next frame all bytes
|
||||
# for nonzero channels will be discarded until
|
||||
# we reach channel 0 again.
|
||||
with m.FSM(name="fifo_postprocess") as fsm:
|
||||
with m.State("NORMAL"):
|
||||
m.d.comb += [
|
||||
out_fifo.r_en.eq(self.usb_stream_out.ready),
|
||||
out_valid.eq(out_fifo.r_rdy)
|
||||
]
|
||||
|
||||
with m.If(~self.audio_in_active & out_fifo.r_rdy):
|
||||
m.next = "DISCARD"
|
||||
|
||||
# frame ongoing
|
||||
with m.Elif(~frame_finished_seen):
|
||||
# start filling if there are not enough enough samples buffered
|
||||
# for one channel set of audio samples
|
||||
last_channel = self.out_channel == (self._max_nr_channels - 1)
|
||||
|
||||
with m.If(last_byte & last_channel & ~fifo_level_sufficient):
|
||||
m.next = "FILL"
|
||||
|
||||
with m.If((self.out_channel != channel_counter)):
|
||||
m.d.comb += [
|
||||
out_fifo.r_en.eq(0),
|
||||
self.usb_stream_out.payload.eq(0),
|
||||
out_valid.eq(1),
|
||||
self.filling.eq(1),
|
||||
]
|
||||
|
||||
# frame finished: discard extraneous samples
|
||||
with m.Else():
|
||||
with m.If(out_fifo.r_rdy & (self.out_channel != 0)):
|
||||
m.d.comb += [
|
||||
out_fifo.r_en.eq(1),
|
||||
out_valid.eq(0),
|
||||
]
|
||||
m.d.sync += [
|
||||
frame_finished_seen.eq(0),
|
||||
byte_pos.eq(0),
|
||||
]
|
||||
m.next = "DISCARD"
|
||||
|
||||
with m.State("DISCARD"):
|
||||
with m.If(out_fifo.r_rdy):
|
||||
m.d.comb += [
|
||||
out_fifo.r_en.eq(1),
|
||||
out_valid.eq(0),
|
||||
self.skipping.eq(1),
|
||||
]
|
||||
with m.If(self.audio_in_active & (self.out_channel == 0)):
|
||||
m.d.comb += out_fifo.r_en.eq(0)
|
||||
m.next = "NORMAL"
|
||||
|
||||
with m.State("FILL"):
|
||||
channel_is_ok = fifo_level_sufficient & (self.out_channel == channel_counter)
|
||||
with m.If(self.frame_finished_in | channel_is_ok):
|
||||
m.next = "NORMAL"
|
||||
with m.Else():
|
||||
m.d.comb += [
|
||||
out_fifo.r_en.eq(0),
|
||||
self.usb_stream_out.payload.eq(0),
|
||||
out_valid.eq(1),
|
||||
self.filling.eq(1),
|
||||
]
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class ChannelsToUSBStreamTest(GatewareTestCase):
|
||||
FRAGMENT_UNDER_TEST = ChannelsToUSBStream
|
||||
FRAGMENT_ARGUMENTS = dict(max_nr_channels=8)
|
||||
|
||||
def send_one_frame(self, sample: int, channel: int, wait=True):
|
||||
yield self.dut.channel_stream_in.channel_nr.eq(channel)
|
||||
yield self.dut.channel_stream_in.payload.eq(sample)
|
||||
yield self.dut.channel_stream_in.valid.eq(1)
|
||||
yield
|
||||
if wait:
|
||||
yield
|
||||
yield
|
||||
yield
|
||||
|
||||
@sync_test_case
|
||||
def test_smoke(self):
|
||||
dut = self.dut
|
||||
yield dut.usb_stream_out.ready.eq(0)
|
||||
yield dut.frame_finished_in.eq(1)
|
||||
yield
|
||||
yield dut.frame_finished_in.eq(0)
|
||||
yield
|
||||
yield
|
||||
yield
|
||||
yield
|
||||
yield
|
||||
yield
|
||||
yield dut.usb_stream_out.ready.eq(1)
|
||||
yield from self.send_one_frame(0x030201, 0, wait=False)
|
||||
yield from self.send_one_frame(0x131211, 1)
|
||||
yield from self.send_one_frame(0x232221, 2)
|
||||
yield from self.send_one_frame(0x333231, 3)
|
||||
# source stream stalls, see if we wait
|
||||
yield dut.channel_stream_in.valid.eq(0)
|
||||
for _ in range(7): yield
|
||||
yield from self.send_one_frame(0x434241, 4)
|
||||
yield from self.send_one_frame(0x535251, 5)
|
||||
yield from self.send_one_frame(0x636261, 6)
|
||||
yield from self.send_one_frame(0x737271, 7, wait=False)
|
||||
# out stream quits early, see if it
|
||||
# consumes extraneous bytes
|
||||
yield dut.usb_stream_out.ready.eq(0)
|
||||
yield
|
||||
for _ in range(15): yield
|
||||
yield dut.frame_finished_in.eq(1)
|
||||
yield
|
||||
yield dut.frame_finished_in.eq(0)
|
||||
for _ in range(35): yield
|
||||
yield from self.send_one_frame(0x030201, 0)
|
||||
yield from self.send_one_frame(0x131211, 1)
|
||||
yield dut.usb_stream_out.ready.eq(1)
|
||||
yield from self.send_one_frame(0x232221, 2)
|
||||
yield from self.send_one_frame(0x333231, 3)
|
||||
yield from self.send_one_frame(0x434241, 4)
|
||||
yield from self.send_one_frame(0x535251, 5)
|
||||
yield from self.send_one_frame(0x636261, 6)
|
||||
yield from self.send_one_frame(0x737271, 7)
|
||||
yield dut.channel_stream_in.valid.eq(0)
|
||||
yield
|
||||
for _ in range(45): yield
|
||||
Reference in New Issue
Block a user