diff --git a/.github/workflows/simulate.yml b/.github/workflows/simulate.yml index 78baf02f5..bc1a7739f 100644 --- a/.github/workflows/simulate.yml +++ b/.github/workflows/simulate.yml @@ -15,17 +15,22 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: [3.9] + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + name: test (${{ matrix.python-version }}) steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Set up PDM + uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox + pdm install + - name: Run tests + run: pdm run test diff --git a/luna/gateware/architecture/car.py b/luna/gateware/architecture/car.py index dade7d969..a70f1e45d 100644 --- a/luna/gateware/architecture/car.py +++ b/luna/gateware/architecture/car.py @@ -12,7 +12,6 @@ from amaranth import Signal, Module, ClockDomain, ClockSignal, Elaboratable, Instance, ResetSignal from ..utils.cdc import stretch_strobe_signal -from ..test import LunaGatewareTestCase, usb_domain_test_case, sync_test_case class PHYResetController(Elaboratable): @@ -95,39 +94,6 @@ def elaborate(self, platform): -class PHYResetControllerTest(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = PHYResetController - - def initialize_signals(self): - yield self.dut.trigger.eq(0) - - @sync_test_case - def test_power_on_reset(self): - - # - # After power-on, the PHY should remain in reset for a while. - # - yield - self.assertEqual((yield self.dut.phy_reset), 1) - - yield from self.advance_cycles(30) - self.assertEqual((yield self.dut.phy_reset), 1) - - yield from self.advance_cycles(60) - self.assertEqual((yield self.dut.phy_reset), 1) - - # - # Then, after the relevant reset time, it should resume being unasserted. - # - yield from self.advance_cycles(31) - self.assertEqual((yield self.dut.phy_reset), 0) - self.assertEqual((yield self.dut.phy_stop), 1) - - yield from self.advance_cycles(120) - self.assertEqual((yield self.dut.phy_stop), 0) - - - class LunaDomainGenerator(Elaboratable, metaclass=ABCMeta): """ Helper that generates the clock domains used in a LUNA board. diff --git a/luna/gateware/debug/console.py b/luna/gateware/debug/console.py index 922ee3842..88fdfa595 100644 --- a/luna/gateware/debug/console.py +++ b/luna/gateware/debug/console.py @@ -8,7 +8,6 @@ from amaranth import Signal, Module, Cat, Elaboratable, Array -from ..test.utils import LunaGatewareTestCase, sync_test_case class DebugConsole(Elaboratable): diff --git a/luna/gateware/debug/ila.py b/luna/gateware/debug/ila.py index 3081b2f2b..bf76632cd 100644 --- a/luna/gateware/debug/ila.py +++ b/luna/gateware/debug/ila.py @@ -10,7 +10,6 @@ import os import sys import math -import unittest import tempfile import subprocess @@ -24,8 +23,7 @@ from ..stream import StreamInterface from ..interface.uart import UARTMultibyteTransmitter -from ..interface.spi import SPIDeviceInterface, SPIBus, SPIGatewareTestCase -from ..test.utils import LunaGatewareTestCase, sync_test_case +from ..interface.spi import SPIDeviceInterface, SPIBus class IntegratedLogicAnalyzer(Elaboratable): @@ -172,105 +170,6 @@ def elaborate(self, platform): return m -class IntegratedLogicAnalyzerTest(LunaGatewareTestCase): - - def instantiate_dut(self): - self.input_a = Signal() - self.input_b = Signal(30) - self.input_c = Signal() - - return IntegratedLogicAnalyzer( - signals=[self.input_a, self.input_b, self.input_c], - sample_depth = 32 - ) - - - def initialize_signals(self): - yield self.input_a .eq(0) - yield self.input_b .eq(0) - yield self.input_c .eq(0) - - - def provide_all_signals(self, value): - all_signals = Cat(self.input_a, self.input_b, self.input_c) - yield all_signals.eq(value) - - - def assert_sample_value(self, address, value): - """ Helper that asserts a ILA sample has a given value. """ - - yield self.dut.captured_sample_number.eq(address) - yield - # Delay a clock to allow the block ram to latch the new value - yield - try: - self.assertEqual((yield self.dut.captured_sample), value) - return - except AssertionError: - pass - - # Generate an appropriate exception. - actual_value = (yield self.dut.captured_sample) - message = "assertion failed: at address 0x{:08x}: {:08x} != {:08x} (expected)".format(address, actual_value, value) - raise AssertionError(message) - - - @sync_test_case - def test_sampling(self): - - # Quick helper that generates simple, repetitive samples. - def sample_value(i): - return i | (i << 8) | (i << 16) | (0xFF << 24) - - yield from self.provide_all_signals(0xDEADBEEF) - yield - - # Before we trigger, we shouldn't be capturing any samples, - # and we shouldn't be complete. - self.assertEqual((yield self.dut.sampling), 0) - self.assertEqual((yield self.dut.complete), 0) - - # Advance a bunch of cycles, and ensure we don't start sampling. - yield from self.advance_cycles(10) - self.assertEqual((yield self.dut.sampling), 0) - - # Set a new piece of data for a couple of cycles. - yield from self.provide_all_signals(0x01234567) - yield - yield from self.provide_all_signals(0x89ABCDEF) - yield - - # Finally, trigger the capture. - yield from self.provide_all_signals(sample_value(0)) - yield from self.pulse(self.dut.trigger, step_after=False) - - yield from self.provide_all_signals(sample_value(1)) - yield - - # After we pulse our trigger strobe, we should be sampling. - self.assertEqual((yield self.dut.sampling), 1) - - # Populate the memory with a variety of interesting signals; - # and continue afterwards for a couple of cycles to make sure - # these don't make it into our sample buffer. - for i in range(2, 34): - yield from self.provide_all_signals(sample_value(i)) - yield - - # We now should be done with our sampling. - self.assertEqual((yield self.dut.sampling), 0) - self.assertEqual((yield self.dut.complete), 1) - - # Validate the memory values that were captured. - for i in range(32): - yield from self.assert_sample_value(i, sample_value(i)) - - # All of those reads shouldn't change our completeness. - self.assertEqual((yield self.dut.sampling), 0) - self.assertEqual((yield self.dut.complete), 1) - - - class SyncSerialILA(Elaboratable): """ Super-simple ILA that reads samples out over a simple unidirectional SPI. Create a receiver for this object by calling apollo_fpga.ila_receiver_for(). @@ -430,60 +329,6 @@ def elaborate(self, platform): return m -class SyncSerialReadoutILATest(SPIGatewareTestCase): - - def instantiate_dut(self): - self.input_signal = Signal(12) - return SyncSerialILA( - signals=[self.input_signal], - sample_depth=16, - clock_polarity=1, - clock_phase=0 - ) - - def initialize_signals(self): - yield self.input_signal.eq(0xF00) - - @sync_test_case - def test_spi_readout(self): - input_signal = self.input_signal - - # Trigger the test while offering our first sample. - yield - yield from self.pulse(self.dut.trigger, step_after=False) - - # Provide the remainder of our samples. - for i in range(1, 16): - yield input_signal.eq(0xF00 | i) - yield - - # Wait a few cycles to account for delays in - # the sampling pipeline. - yield from self.advance_cycles(5) - - # We've now captured a full set of samples. - # We'll test reading them out. - self.assertEqual((yield self.dut.complete), 1) - - # Start the transaction, and exchange 16 bytes of data. - yield self.dut.spi.cs.eq(1) - yield - - # Read our our result over SPI... - data = yield from self.spi_exchange_data(b"\0" * 32) - - # ... and ensure it matches what was sampled. - i = 0 - while data: - datum = data[0:4] - del data[0:4] - - expected = b"\x00\x00\x0f" + bytes([i]) - self.assertEqual(datum, expected) - i += 1 - - - class StreamILA(Elaboratable): """ Super-simple ILA that outputs its samples over a Stream. @@ -674,53 +519,6 @@ def elaborate(self, platform): return m -class StreamILATest(LunaGatewareTestCase): - - def instantiate_dut(self): - self.input_signal = Signal(12) - return StreamILA( - signals=[self.input_signal], - sample_depth=16 - ) - - def initialize_signals(self): - yield self.input_signal.eq(0xF00) - - @sync_test_case - def test_stream_readout(self): - input_signal = self.input_signal - stream = self.dut.stream - - # Trigger the ILA with the first sample - yield - yield from self.pulse(self.dut.trigger, step_after=False) - - # Fill up the ILA with the remaining samples - for i in range(1, 16): - yield input_signal.eq(0xF00 | i) - yield - - # Wait a few cycles to allow the ILA to fully finish processing - yield from self.advance_cycles(6) - # Stream should now be presenting valid data - self.assertEqual((yield stream.valid), 1) - - # Now we want to stream out the samples from the ILA - yield stream.ready.eq(1) - yield - self.assertEqual((yield stream.first), 1) - - # Read out data from the stream until it signals completion - data = [] - while not (yield stream.last): - if (yield stream.valid): - data.append((yield stream.payload)) - yield - - # Match read data to what should have been sampled - for i, datum in enumerate(data): - self.assertEqual(datum, 0xF00 | i) - class AsyncSerialILA(Elaboratable): """ Super-simple ILA that reads samples out over a UART connection. @@ -1031,7 +829,3 @@ def _read_samples(self): # Fetch all of our samples from the given device. all_samples = self._port.read(total_to_read) return list(self._split_samples(all_samples)) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/interface/i2c.py b/luna/gateware/interface/i2c.py index 9c7b69d0e..0fedfb746 100644 --- a/luna/gateware/interface/i2c.py +++ b/luna/gateware/interface/i2c.py @@ -12,8 +12,6 @@ from amaranth.lib.cdc import FFSynchronizer from amaranth.hdl.rec import Record, DIR_FANIN, DIR_FANOUT -import unittest -from ..test import LunaGatewareTestCase, sync_test_case __all__ = ["I2CBus", "I2CInitiator", "I2CRegisterInterface"] @@ -474,160 +472,3 @@ def stb_x(state, next_state, *exprs, bit7_next_state=None): return m - -class I2CInitiatorTestbench(I2CInitiator): - def __init__(self, pads, period_cyc, clk_stretch=True): - super().__init__(pads, period_cyc, clk_stretch) - self.scl_o = Signal(reset=1) # used to override values from testbench - self.sda_o = Signal(reset=1) - - def elaborate(self, platform): - m = super().elaborate(platform) - m.d.comb += [ - self.bus.scl_t.i.eq((self.bus.scl_t.o | ~self.bus.scl_t.oe) & self.scl_o), - self.bus.sda_t.i.eq((self.bus.sda_t.o | ~self.bus.sda_t.oe) & self.sda_o), - ] - return m - -class TestI2CInitiator(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = I2CInitiatorTestbench - FRAGMENT_ARGUMENTS = { "pads": I2CBus(), "period_cyc": 16 } - - def wait_condition(self, strobe): - yield from self.wait_until(strobe, timeout=3*self.dut.period_cyc) - - def start(self): - yield from self.pulse(self.dut.start) - yield from self.wait_condition(self.dut.bus.start) - - def stop(self): - yield from self.pulse(self.dut.stop) - yield from self.wait_condition(self.dut.bus.stop) - - @sync_test_case - def test_start(self): - yield from self.start() - self.assertEqual((yield self.dut.busy), 0) - - @sync_test_case - def test_repeated_start(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.start() - yield from self.wait_condition(self.dut.bus.start) - self.assertEqual((yield self.dut.busy), 0) - - @sync_test_case - def test_stop(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.stop() - self.assertEqual((yield self.dut.busy), 0) - - def write(self, data, bits, ack): - yield self.dut.data_i.eq(data) - yield from self.pulse(self.dut.write) - for n, bit in enumerate(bits): - yield - yield - yield from self.wait_condition(self.dut.bus.scl_i == 0) - yield from self.wait_condition(self.dut.bus.scl_i == 1) - self.assertEqual((yield self.dut.bus.sda_i), bit) - yield - yield from self.advance_cycles(self.dut.period_cyc // 2) - yield - yield - yield from self.wait_condition(self.dut.bus.scl_i == 0) - yield self.dut.sda_o.eq(not ack) - yield from self.wait_condition(self.dut.bus.scl_i == 1) - yield self.dut.sda_o.eq(1) - self.assertEqual((yield self.dut.busy), 1) - yield from self.advance_cycles(self.dut.period_cyc // 2) - yield - yield - yield - yield - self.assertEqual((yield self.dut.busy), 0) - self.assertEqual((yield self.dut.ack_o), ack) - - @sync_test_case - def test_write_ack(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.write(0xA5, [1, 0, 1, 0, 0, 1, 0, 1], 1) - - @sync_test_case - def test_write_nak(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.write(0x5A, [0, 1, 0, 1, 1, 0, 1, 0], 0) - - @sync_test_case - def test_write_tx(self): - yield from self.start() - yield from self.write(0x55, [0, 1, 0, 1, 0, 1, 0, 1], 1) - yield from self.write(0x33, [0, 0, 1, 1, 0, 0, 1, 1], 0) - yield from self.stop() - yield - yield - self.assertEqual((yield self.dut.bus.sda_i), 1) - self.assertEqual((yield self.dut.bus.scl_i), 1) - - def read(self, data, bits, ack): - yield self.dut.ack_i.eq(ack) - yield from self.pulse(self.dut.read) - for n, bit in enumerate(bits): - yield - yield - yield from self.wait_condition(self.dut.bus.scl_i == 0) - yield self.dut.sda_o.eq(bit) - yield from self.wait_condition(self.dut.bus.scl_i == 1) - yield - yield self.dut.sda_o.eq(1) - yield from self.advance_cycles(self.dut.period_cyc // 2) - yield - yield - yield from self.wait_condition(self.dut.bus.scl_i == 0) - yield from self.wait_condition(self.dut.bus.scl_i == 1) - self.assertEqual((yield self.dut.bus.sda_i), not ack) - self.assertEqual((yield self.dut.busy), 1) - yield from self.advance_cycles(self.dut.period_cyc // 2) - yield - yield - yield - yield - self.assertEqual((yield self.dut.busy), 0) - self.assertEqual((yield self.dut.data_o), data) - - @sync_test_case - def test_read_ack(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.read(0xA5, [1, 0, 1, 0, 0, 1, 0, 1], 1) - - @sync_test_case - def test_read_nak(self): - yield self.dut.bus.sda_o.eq(0) - yield - yield - yield from self.read(0x5A, [0, 1, 0, 1, 1, 0, 1, 0], 0) - - @sync_test_case - def test_read_tx(self): - yield from self.start() - yield from self.read(0x55, [0, 1, 0, 1, 0, 1, 0, 1], 1) - yield from self.read(0x33, [0, 0, 1, 1, 0, 0, 1, 1], 0) - yield from self.stop() - yield - yield - self.assertEqual((yield self.dut.bus.sda_i), 1) - self.assertEqual((yield self.dut.bus.scl_i), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/interface/psram.py b/luna/gateware/interface/psram.py index 5e1f2c75f..af31f80a7 100644 --- a/luna/gateware/interface/psram.py +++ b/luna/gateware/interface/psram.py @@ -6,14 +6,10 @@ """ Interfaces to LUNA's PSRAM chips.""" -import unittest - from amaranth import Const, Signal, Module, Cat, Elaboratable, Record, ClockSignal, ResetSignal, Instance from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT from amaranth.lib.cdc import FFSynchronizer -from ..test.utils import LunaGatewareTestCase, sync_test_case - class HyperBusPHY(Record): """ Record representing a 16-bit HyperBus interface for use with a 2:1 PHY module. """ @@ -310,178 +306,6 @@ def elaborate(self, platform): return m -class TestHyperRAMInterface(LunaGatewareTestCase): - - def instantiate_dut(self): - # Create a record that recreates the layout of our RAM signals. - self.ram_signals = HyperBusPHY() - - # Create our HyperRAM interface... - return HyperRAMInterface(phy=self.ram_signals) - - - def assert_clock_pulses(self, times=1): - """ Function that asserts we get a specified number of clock pulses. """ - - for _ in range(times): - yield - self.assertEqual((yield self.ram_signals.clk_en), 1) - - - @sync_test_case - def test_register_write(self): - - # Before we transact, CS should be de-asserted, and RWDS and DQ should be undriven. - yield - self.assertEqual((yield self.ram_signals.cs), 0) - self.assertEqual((yield self.ram_signals.dq.e), 0) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - - yield from self.advance_cycles(10) - self.assertEqual((yield self.ram_signals.cs), 0) - - # Request a register write to ID register 0. - yield self.dut.perform_write .eq(1) - yield self.dut.register_space .eq(1) - yield self.dut.address .eq(0x00BBCCDD) - yield self.dut.start_transfer .eq(1) - yield self.dut.final_word .eq(1) - yield self.dut.write_data .eq(0xBEEF) - - # Simulate the RAM requesting a extended latency. - yield self.ram_signals.rwds.i .eq(1) - yield - - # Ensure that upon requesting, CS goes high, and our clock starts low. - yield - self.assertEqual((yield self.ram_signals.cs), 1) - self.assertEqual((yield self.ram_signals.clk_en), 0) - - # Drop our "start request" line somewhere during the transaction; - # so we don't immediately go into the next transfer. - yield self.dut.start_transfer.eq(0) - - # We should then move to shifting out our first command word, - # which means we're driving DQ with the first word of our command. - yield - yield - self.assertEqual((yield self.ram_signals.cs), 1) - self.assertEqual((yield self.ram_signals.clk_en), 1) - self.assertEqual((yield self.ram_signals.dq.e), 1) - self.assertEqual((yield self.ram_signals.dq.o), 0x6017) - - # This should continue until we've shifted out a full command. - yield - self.assertEqual((yield self.ram_signals.dq.o), 0x799B) - yield - self.assertEqual((yield self.ram_signals.dq.o), 0x0005) - - # Check that we've been driving our output this whole time, - # and haven't been driving RWDS. - self.assertEqual((yield self.ram_signals.dq.e), 1) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - yield - - # For a _register_ write, there shouldn't be latency period. - # This means we should continue driving DQ... - self.assertEqual((yield self.ram_signals.dq.e), 1) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - self.assertEqual((yield self.ram_signals.dq.o), 0xBEEF) - - - - @sync_test_case - def test_register_read(self): - - # Before we transact, CS should be de-asserted, and RWDS and DQ should be undriven. - yield - self.assertEqual((yield self.ram_signals.cs), 0) - self.assertEqual((yield self.ram_signals.dq.e), 0) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - - yield from self.advance_cycles(10) - self.assertEqual((yield self.ram_signals.cs), 0) - - # Request a register read of ID register 0. - yield self.dut.perform_write .eq(0) - yield self.dut.register_space .eq(1) - yield self.dut.address .eq(0x00BBCCDD) - yield self.dut.start_transfer .eq(1) - yield self.dut.final_word .eq(1) - - # Simulate the RAM requesting a extended latency. - yield self.ram_signals.rwds.i .eq(1) - yield - - # Ensure that upon requesting, CS goes high, and our clock starts low. - yield - self.assertEqual((yield self.ram_signals.cs), 1) - self.assertEqual((yield self.ram_signals.clk_en), 0) - - # Drop our "start request" line somewhere during the transaction; - # so we don't immediately go into the next transfer. - yield self.dut.start_transfer.eq(0) - - # We should then move to shifting out our first command word, - # which means we're driving DQ with the first word of our command. - yield - yield - self.assertEqual((yield self.ram_signals.cs), 1) - self.assertEqual((yield self.ram_signals.clk_en), 1) - self.assertEqual((yield self.ram_signals.dq.e), 1) - self.assertEqual((yield self.ram_signals.dq.o), 0xe017) - - # This should continue until we've shifted out a full command. - yield - self.assertEqual((yield self.ram_signals.dq.o), 0x799B) - yield - self.assertEqual((yield self.ram_signals.dq.o), 0x0005) - - # Check that we've been driving our output this whole time, - # and haven't been driving RWDS. - self.assertEqual((yield self.ram_signals.dq.e), 1) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - - # Once we finish scanning out the word, we should stop driving - # the data lines, and should finish two latency periods before - # sending any more data. - yield - self.assertEqual((yield self.ram_signals.dq.e), 0) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - self.assertEqual((yield self.ram_signals.clk_en), 1) - - # By this point, the RAM will drive RWDS low. - yield self.ram_signals.rwds.i.eq(0) - - # Ensure the clock still ticking... - yield - self.assertEqual((yield self.ram_signals.clk_en), 1) - - # ... and remains so for the remainder of the latency period. - yield from self.assert_clock_pulses(14) - - # Now, shift in a pair of data words. - yield self.ram_signals.dq.i.eq(0xCAFE) - yield self.ram_signals.rwds.i.eq(0b10) - yield - - # Once this finished, we should have a result on our data out. - self.assertEqual((yield self.dut.read_data), 0xCAFE) - self.assertEqual((yield self.dut.read_ready ), 1) - - yield - yield - self.assertEqual((yield self.ram_signals.cs), 0) - self.assertEqual((yield self.ram_signals.dq.e), 0) - self.assertEqual((yield self.ram_signals.rwds.e), 0) - - # Ensure that our clock drops back to '0' during idle cycles. - yield from self.advance_cycles(2) - self.assertEqual((yield self.ram_signals.clk_en), 0) - - # TODO: test recovery time - - class HyperRAMPHY(Elaboratable): """ Gateware interface to HyperRAM series self-refreshing DRAM chips. @@ -581,7 +405,3 @@ def elaborate(self, platform): ] return m - -if __name__ == "__main__": - unittest.main() - diff --git a/luna/gateware/interface/spi.py b/luna/gateware/interface/spi.py index 693dc0962..a12bb5e01 100644 --- a/luna/gateware/interface/spi.py +++ b/luna/gateware/interface/spi.py @@ -6,8 +6,6 @@ """ SPI and derived interfaces. """ -import unittest - from amaranth import Signal, Module, Cat, Elaboratable, Record from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT @@ -241,51 +239,6 @@ def spi_exchange_data(self, data, msb_first=True): -class SPIDeviceInterfaceTest(SPIGatewareTestCase): - FRAGMENT_UNDER_TEST = SPIDeviceInterface - FRAGMENT_ARGUMENTS = dict(word_size=16, clock_polarity=1) - - def initialize_signals(self): - yield self.dut.spi.cs.eq(0) - - - @sync_test_case - def test_spi_interface(self): - - # Ensure that we don't complete a word while CS is deasserted. - for _ in range(10): - self.assertEqual((yield self.dut.word_complete), 0) - yield - - # Set the word we're expected to send, and then assert CS. - yield self.dut.word_out.eq(0xABCD) - yield - - yield self.dut.spi.cs.eq(1) - yield - - # Verify that the SPI in/out behavior is what we expect. - response = yield from self.spi_exchange_data(b"\xCA\xFE") - self.assertEqual(response, b"\xAB\xCD") - self.assertEqual((yield self.dut.word_in), 0xCAFE) - - - @sync_test_case - def test_spi_transmit_second_word(self): - - # Set the word we're expected to send, and then assert CS. - yield self.dut.word_out.eq(0x0f00) - yield - - yield self.dut.spi.cs.eq(1) - yield - - # Verify that the SPI in/out behavior is what we expect. - response = yield from self.spi_exchange_data(b"\x00\x00") - self.assertEqual(response, b"\x0F\x00") - - - class SPICommandInterface(Elaboratable): """ Variant of an SPIDeviceInterface that accepts command-prefixed data. @@ -728,61 +681,6 @@ def elaborate(self, platform): -class SPIRegisterInterfaceTest(SPIGatewareTestCase): - """ Tests for the SPI command interface. """ - - def instantiate_dut(self): - - self.write_strobe = Signal() - - # Create a register and sample dataset to work with. - dut = SPIRegisterInterface(default_read_value=0xDEADBEEF) - dut.add_register(2, write_strobe=self.write_strobe) - - return dut - - - def initialize_signals(self): - # Start off with our clock low and the transaction idle. - yield self.dut.spi.sck.eq(0) - yield self.dut.spi.cs.eq(0) - - - @sync_test_case - def test_undefined_read_behavior(self): - data = yield from self.spi_exchange_data([0, 1, 0, 0, 0, 0]) - self.assertEqual(bytes(data), b"\x00\x00\xde\xad\xbe\xef") - - - @sync_test_case - def test_write_behavior(self): - - # Send a write command... - data = yield from self.spi_exchange_data(b"\x80\x02\x12\x34\x56\x78") - self.assertEqual(bytes(data), b"\x00\x00\x00\x00\x00\x00") - - # ... and then read the relevant data back. - data = yield from self.spi_exchange_data(b"\x00\x02\x12\x34\x56\x78") - self.assertEqual(bytes(data), b"\x00\x00\x12\x34\x56\x78") - - - @sync_test_case - def test_aborted_write_behavior(self): - - # Set an initial value... - data = yield from self.spi_exchange_data(b"\x80\x02\x12\x34\x56\x78") - - # ... and then perform an incomplete write. - data = yield from self.spi_exchange_data(b"\x80\x02\xAA\xBB") - - # We should return to being idle after CS is de-asserted... - yield - self.assertEqual((yield self.dut.idle), 1) - - # ... and our register data should not have changed. - data = yield from self.spi_exchange_data(b"\x00\x02\x12\x34\x56\x78") - self.assertEqual(bytes(data), b"\x00\x00\x12\x34\x56\x78") - class SPIMultiplexer(Elaboratable): """ Gateware that assists in connecting multiple SPI busses to the same shared lines. """ @@ -824,10 +722,3 @@ def elaborate(self, platform): return m - - - - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/interface/uart.py b/luna/gateware/interface/uart.py index e6aa73edb..a34da7f0d 100644 --- a/luna/gateware/interface/uart.py +++ b/luna/gateware/interface/uart.py @@ -6,13 +6,9 @@ """ UART interface gateware.""" -import unittest - from amaranth import Elaboratable, Module, Signal, Cat - from ..stream import StreamInterface -from ..test import LunaGatewareTestCase, sync_test_case class UARTTransmitter(Elaboratable): @@ -135,78 +131,6 @@ def elaborate(self, platform): return m -class UARTTransmitterTest(LunaGatewareTestCase): - DIVISOR = 10 - - FRAGMENT_UNDER_TEST = UARTTransmitter - FRAGMENT_ARGUMENTS = dict(divisor=DIVISOR) - - - def advance_half_bit(self): - yield from self.advance_cycles(self.DIVISOR // 2) - - def advance_bit(self): - yield from self.advance_cycles(self.DIVISOR) - - - def assert_data_sent(self, byte_expected): - dut = self.dut - - # Our start bit should remain present until the next bit period. - yield from self.advance_half_bit() - self.assertEqual((yield dut.tx), 0) - - # We should then see each bit of our data, LSB first. - bits = [int(i) for i in f"{byte_expected:08b}"] - for bit in bits[::-1]: - yield from self.advance_bit() - self.assertEqual((yield dut.tx), bit) - - # Finally, we should see a stop bit. - yield from self.advance_bit() - self.assertEqual((yield dut.tx), 1) - - - @sync_test_case - def test_burst_transmit(self): - dut = self.dut - stream = dut.stream - - # We should remain idle until a transmit is requested... - yield from self.advance_cycles(10) - self.assertEqual((yield dut.idle), 1) - self.assertEqual((yield dut.stream.ready), 1) - - # ... and our tx line should idle high. - self.assertEqual((yield dut.tx), 1) - - # First, transmit 0x55 (maximum transition rate). - yield stream.payload.eq(0x55) - yield stream.valid.eq(1) - - # We should see our data become accepted; and we - # should see a start bit. - yield - self.assertEqual((yield stream.ready), 1) - yield - self.assertEqual((yield dut.tx), 0) - - # Provide our next byte of data once the current - # one has been accepted. Changing this before the tests - # below ensures that we validate that data is latched properly. - yield stream.payload.eq(0x66) - - # Ensure we get our data correctly. - yield from self.assert_data_sent(0x55) - yield from self.assert_data_sent(0x66) - - # Stop transmitting after the next frame. - yield stream.valid.eq(0) - - # Ensure we actually stop. - yield from self.advance_bit() - self.assertEqual((yield dut.idle), 1) - class UARTMultibyteTransmitter(Elaboratable): """ UART transmitter capable of sending wide words. @@ -321,68 +245,3 @@ def elaborate(self, platform): return m - - -class UARTMultibyteTransmitterTest(LunaGatewareTestCase): - DIVISOR = 10 - - FRAGMENT_UNDER_TEST = UARTMultibyteTransmitter - FRAGMENT_ARGUMENTS = dict(divisor=DIVISOR, byte_width=4) - - - def advance_half_bit(self): - yield from self.advance_cycles(self.DIVISOR // 2) - - def advance_bit(self): - yield from self.advance_cycles(self.DIVISOR) - - - def assert_data_sent(self, byte_expected): - dut = self.dut - - # Our start bit should remain present until the next bit period. - yield from self.advance_half_bit() - self.assertEqual((yield dut.tx), 0) - - # We should then see each bit of our data, LSB first. - bits = [int(i) for i in f"{byte_expected:08b}"] - for bit in bits[::-1]: - yield from self.advance_bit() - self.assertEqual((yield dut.tx), bit) - - # Finally, we should see a stop bit. - yield from self.advance_bit() - self.assertEqual((yield dut.tx), 1) - - yield from self.advance_cycles(2) - - - @sync_test_case - def test_burst_transmit(self): - dut = self.dut - stream = dut.stream - - # We should remain idle until a transmit is requested... - yield from self.advance_cycles(10) - self.assertEqual((yield dut.idle), 1) - self.assertEqual((yield dut.stream.ready), 1) - - # Transmit a four-byte word. - yield stream.payload.eq(0x11223355) - yield stream.valid.eq(1) - - # We should see our data become accepted; and we - # should see a start bit. - yield - self.assertEqual((yield stream.ready), 1) - - # Ensure we get our data correctly, and that our transmitter - # isn't accepting data mid-frame. - yield from self.assert_data_sent(0x55) - self.assertEqual((yield stream.ready), 0) - yield from self.assert_data_sent(0x33) - - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/interface/ulpi.py b/luna/gateware/interface/ulpi.py index 1e8248ace..5f947736d 100644 --- a/luna/gateware/interface/ulpi.py +++ b/luna/gateware/interface/ulpi.py @@ -7,14 +7,11 @@ """ ULPI interfacing hardware. """ -import unittest - from amaranth import Signal, Module, Cat, Elaboratable, ClockSignal, \ Record, ResetSignal, Const from amaranth.hdl.rec import Record, DIR_FANIN, DIR_FANOUT, DIR_NONE from ..utils.io import delay -from ..test import LunaGatewareTestCase, usb_domain_test_case, sync_test_case class ULPIInterface(Record): @@ -257,161 +254,6 @@ def elaborate(self, platform): return m - -class TestULPIRegisters(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = ULPIRegisterWindow - - USB_CLOCK_FREQUENCY = 60e6 - SYNC_CLOCK_FREQUENCY = None - - def initialize_signals(self): - yield self.dut.ulpi_dir.eq(0) - - yield self.dut.read_request.eq(0) - yield self.dut.write_request.eq(0) - - - @usb_domain_test_case - def test_idle_behavior(self): - """ Ensure we apply a NOP whenever we're not actively performing a command. """ - self.assertEqual((yield self.dut.ulpi_data_out), 0) - - - @usb_domain_test_case - def test_register_read(self): - """ Validates a register read. """ - - # Poison the register value with a fail value (0xBD). - yield self.dut.ulpi_data_in.eq(0xBD) - - # Set up a read request. - yield self.dut.address.eq(0) - yield - - # After a read request, we should be busy... - yield from self.pulse(self.dut.read_request) - self.assertEqual((yield self.dut.busy), 1) - - # ... and then, since dir is unasserted, we should have a read command. - yield - self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) - - # We should continue to present the command... - yield from self.advance_cycles(10) - self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) - self.assertEqual((yield self.dut.busy), 1) - - # ... until the host accepts it. - yield self.dut.ulpi_next.eq(1) - yield - - # We should then wait for a single bus turnaround cycle before reading. - yield - - # And then should read whatever value is present. - yield self.dut.ulpi_data_in.eq(0x07) - yield - yield - self.assertEqual((yield self.dut.read_data), 0x07) - - # Finally, we should return to idle. - self.assertEqual((yield self.dut.busy), 0) - - - @usb_domain_test_case - def test_interrupted_read(self): - """ Validates how a register read works when interrupted by a change in DIR. """ - - # Set up a read request while DIR is asserted. - yield self.dut.ulpi_dir.eq(1) - yield self.dut.address.eq(0) - yield from self.pulse(self.dut.read_request) - - # We shouldn't try to output anything until DIR is de-asserted. - yield from self.advance_cycles(1) - self.assertEqual((yield self.dut.ulpi_out_req), 0) - yield from self.advance_cycles(10) - self.assertEqual((yield self.dut.ulpi_out_req), 0) - - # De-assert DIR, and let the platform apply a read command. - yield self.dut.ulpi_dir.eq(0) - yield from self.advance_cycles(2) - self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) - - # Assert DIR again; interrupting the read. This should bring - # the platform back to its "waiting for the bus" state. - yield self.dut.ulpi_dir.eq(1) - yield from self.advance_cycles(2) - self.assertEqual((yield self.dut.ulpi_out_req), 0) - - # Clear DIR, and validate that the device starts driving the command again - yield self.dut.ulpi_dir.eq(0) - yield from self.advance_cycles(2) - self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) - - # Apply NXT so the read can finally continue. - yield self.dut.ulpi_next.eq(1) - yield - - # We should then wait for a single bus turnaround cycle before reading. - yield - - # And then should read whatever value is present. - yield self.dut.ulpi_data_in.eq(0x07) - yield - yield - self.assertEqual((yield self.dut.read_data), 0x07) - - # Finally, we should return to idle. - self.assertEqual((yield self.dut.busy), 0) - - - @usb_domain_test_case - def test_register_write(self): - - # Set up a write request. - yield self.dut.address.eq(0b10) - yield self.dut.write_data.eq(0xBC) - yield - - # Starting the request should make us busy. - yield from self.pulse(self.dut.write_request) - self.assertEqual((yield self.dut.busy), 1) - - # ... and then, since dir is unasserted, we should have a write command. - yield - self.assertEqual((yield self.dut.ulpi_data_out), 0b10000010) - - # We should continue to present the command... - yield from self.advance_cycles(10) - self.assertEqual((yield self.dut.ulpi_data_out), 0b10000010) - self.assertEqual((yield self.dut.busy), 1) - - # ... until the host accepts it. - yield self.dut.ulpi_next.eq(1) - yield - - # We should then present the data to be written... - yield self.dut.ulpi_next.eq(0) - yield - self.assertEqual((yield self.dut.ulpi_data_out), 0xBC) - - # ... and continue doing so until the host accepts it... - yield from self.advance_cycles(10) - self.assertEqual((yield self.dut.ulpi_data_out), 0xBC) - - yield self.dut.ulpi_next.eq(1) - yield from self.advance_cycles(2) - - # ... at which point stop should be asserted for one cycle. - self.assertEqual((yield self.dut.ulpi_stop), 1) - yield - - # Finally, we should go idle. - self.assertEqual((yield self.dut.ulpi_stop), 0) - self.assertEqual((yield self.dut.busy), 0) - - class ULPIRxEventDecoder(Elaboratable): """ Simple piece of gateware that tracks receive events. @@ -513,77 +355,6 @@ def elaborate(self, platform): return m -class ULPIRxEventDecoderTest(LunaGatewareTestCase): - - USB_CLOCK_FREQUENCY = 60e6 - SYNC_CLOCK_FREQUENCY = None - - def instantiate_dut(self): - - self.ulpi = Record([ - ("dir", [ - ("i", 1), - ]), - ("nxt", [ - ("i", 1), - ]), - ("data", [ - ("i", 8), - ]) - ]) - - return ULPIRxEventDecoder(ulpi_bus=self.ulpi) - - - def initialize_signals(self): - yield self.ulpi.dir.i.eq(0) - yield self.ulpi.nxt.i.eq(0) - yield self.ulpi.data.i.eq(0) - yield self.dut.register_operation_in_progress.eq(0) - - - @usb_domain_test_case - def test_decode(self): - - # Provide a test value. - yield self.ulpi.data.i.eq(0xAB) - - # First, set DIR and NXT at the same time, and verify that we - # don't register an RxEvent. - yield self.ulpi.dir.i.eq(1) - yield self.ulpi.nxt.i.eq(1) - - yield from self.advance_cycles(5) - self.assertEqual((yield self.dut.last_rx_command), 0x00) - - # Nothing should change when we drop DIR and NXT. - yield self.ulpi.dir.i.eq(0) - yield self.ulpi.nxt.i.eq(0) - yield - self.assertEqual((yield self.dut.last_rx_command), 0x00) - - - # Setting DIR but not NXT should trigger an RxEvent; but not - # until one cycle of "bus turnaround" has passed. - yield self.ulpi.dir.i.eq(1) - - yield self.ulpi.data.i.eq(0x12) - yield - self.assertEqual((yield self.dut.last_rx_command), 0x00) - - yield self.ulpi.data.i.eq(0b00011110) - yield from self.advance_cycles(2) - - self.assertEqual((yield self.dut.last_rx_command), 0b00011110) - - # Validate that we're decoding this RxCommand correctly. - self.assertEqual((yield self.dut.line_state), 0b10) - self.assertEqual((yield self.dut.vbus_valid), 1) - self.assertEqual((yield self.dut.rx_active), 1) - self.assertEqual((yield self.dut.rx_error), 0) - self.assertEqual((yield self.dut.host_disconnect), 0) - - class ULPIControlTranslator(Elaboratable): """ Gateware that translates ULPI control signals to their UTMI equivalents. @@ -753,69 +524,6 @@ def elaborate(self, platform): return m -class ControlTranslatorTest(LunaGatewareTestCase): - - USB_CLOCK_FREQUENCY = 60e6 - SYNC_CLOCK_FREQUENCY = None - - def instantiate_dut(self): - self.reg_window = ULPIRegisterWindow() - return ULPIControlTranslator(register_window=self.reg_window, own_register_window=True) - - - def initialize_signals(self): - dut = self.dut - - # Initialize our register signals to their default values. - yield dut.xcvr_select.eq(1) - yield dut.dm_pulldown.eq(1) - yield dut.dp_pulldown.eq(1) - yield dut.use_external_vbus_indicator.eq(0) - yield dut.bus_idle.eq(1) - - - @usb_domain_test_case - def test_multiwrite_behavior(self): - - # Give our initialization some time to settle, - # and verify that we haven't initiated anyting in that interim. - yield from self.advance_cycles(10) - self.assertEqual((yield self.reg_window.write_request), 0) - - # Change signals that span two registers. - yield self.dut.op_mode.eq(0b11) - yield self.dut.dp_pulldown.eq(0) - yield self.dut.dm_pulldown.eq(0) - yield - yield - - # Once we've changed these, we should start trying to apply - # our new value to the function control register. - self.assertEqual((yield self.reg_window.address), 0x04) - self.assertEqual((yield self.reg_window.write_data), 0b01011001) - - # which should occur until the data and address are accepted. - yield self.reg_window.ulpi_next.eq(1) - yield from self.wait_until(self.reg_window.done, timeout=10) - yield - yield - - # We should then experience a write to the function control register. - self.assertEqual((yield self.reg_window.address), 0x0A) - self.assertEqual((yield self.reg_window.write_data), 0b00000000) - - # Wait for that action to complete.. - yield self.reg_window.ulpi_next.eq(1) - yield from self.wait_until(self.reg_window.done, timeout=10) - yield - yield - - # After which we shouldn't be trying to write anything at all. - self.assertEqual((yield self.reg_window.address), 0) - self.assertEqual((yield self.reg_window.write_data), 0) - self.assertEqual((yield self.reg_window.write_request), 0) - - class ULPITransmitTranslator(Elaboratable): """ Accepts UTMI transmit signals, and converts them into ULPI equivalents. @@ -941,97 +649,6 @@ def elaborate(self, platform): return m -class ULPITransmitTranslatorTest(LunaGatewareTestCase): - USB_CLOCK_FREQUENCY=60e6 - SYNC_CLOCK_FREQUENCY=None - - FRAGMENT_UNDER_TEST = ULPITransmitTranslator - - def initialize_signals(self): - yield self.dut.bus_idle.eq(1) - - @usb_domain_test_case - def test_simple_transmit(self): - dut = self.dut - - # We shouldn't try to transmit until we have a transmit request. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.ulpi_out_req), 0) - - # Present a simple SOF PID. - yield dut.tx_valid.eq(1) - yield dut.tx_data.eq(0xA5) - yield - - # Our PID should have been translated into a transmit request, with - # our PID in the lower nibble. - self.assertEqual((yield dut.ulpi_data_out), 0b01000101) - self.assertEqual((yield dut.tx_ready), 0) - self.assertEqual((yield dut.ulpi_stp), 0) - yield - self.assertEqual((yield dut.ulpi_out_req), 1) - - # Our PID should remain there until we indicate we're ready. - self.advance_cycles(10) - self.assertEqual((yield dut.ulpi_data_out), 0b01000101) - - # Once we're ready, we should accept the data from the link and continue. - yield dut.ulpi_nxt.eq(1) - yield - self.assertEqual((yield dut.tx_ready), 1) - yield dut.tx_data.eq(0x11) - yield - - # At which point we should present the relevant data directly. - yield - self.assertEqual((yield dut.ulpi_data_out), 0x11) - - # Finally, once we stop our transaction... - yield dut.tx_valid.eq(0) - yield - - # ... we should get a cycle of STP. - self.assertEqual((yield dut.ulpi_data_out), 0) - self.assertEqual((yield dut.ulpi_stp), 1) - - # ... followed by idle. - yield - self.assertEqual((yield dut.ulpi_stp), 0) - - - @usb_domain_test_case - def test_handshake(self): - dut = self.dut - - # Present a simple ACK PID. - yield dut.tx_valid.eq(1) - yield dut.tx_data.eq(0b11010010) - yield - - # Our PID should have been translated into a transmit request, with - # our PID in the lower nibble. - self.assertEqual((yield dut.ulpi_data_out), 0b01000010) - self.assertEqual((yield dut.tx_ready), 0) - self.assertEqual((yield dut.ulpi_stp), 0) - - # Once the PHY accepts the data, it'll assert NXT. - yield dut.ulpi_nxt.eq(1) - yield - self.assertEqual((yield dut.ulpi_out_req), 1) - - # ... which will trigger the transmitter to drop tx_valid. - yield dut.tx_valid.eq(0) - - # ... we should get a cycle of STP. - yield - #self.assertEqual((yield dut.ulpi_data_out), 0) - #self.assertEqual((yield dut.ulpi_stp), 1) - - # ... followed by idle. - yield - self.assertEqual((yield dut.ulpi_stp), 0) - - class UTMITranslator(Elaboratable): """ Gateware that translates a ULPI interface into a simpler UTMI one. @@ -1323,7 +940,3 @@ def elaborate(self, platform): ] return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/memory.py b/luna/gateware/memory.py index a28bbd446..54befb2c0 100644 --- a/luna/gateware/memory.py +++ b/luna/gateware/memory.py @@ -8,15 +8,10 @@ This module contains definitions of memory units that work well for USB applications. """ -import unittest - from amaranth import Elaboratable, Module, Signal, Memory from amaranth.hdl.xfrm import DomainRenamer -from .test import LunaGatewareTestCase, sync_test_case - - class TransactionalizedFIFO(Elaboratable): """ Transactionalized, buffer first-in-first-out queue. @@ -226,128 +221,3 @@ def elaborate(self, platform): m = DomainRenamer({"sync": self.domain})(m) return m - - - -class TransactionalizedFIFOTest(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = TransactionalizedFIFO - FRAGMENT_ARGUMENTS = {'width': 8, 'depth': 16} - - def initialize_signals(self): - yield self.dut.write_en.eq(0) - - @sync_test_case - def test_simple_fill(self): - dut = self.dut - - # Our FIFO should start off empty; and with a full depth of free space. - self.assertEqual((yield dut.empty), 1) - self.assertEqual((yield dut.full), 0) - self.assertEqual((yield dut.space_available), 16) - - # If we add a byte to the queue... - yield dut.write_data.eq(0xAA) - yield from self.pulse(dut.write_en) - - # ... we should have less space available ... - self.assertEqual((yield dut.space_available), 15) - - # ... but we still should be "empty", as we won't have data to read until we commit. - self.assertEqual((yield dut.empty), 1) - - # Once we _commit_ our write, we should suddenly have data to read. - yield from self.pulse(dut.write_commit) - self.assertEqual((yield dut.empty), 0) - - # If we read a byte, we should see the FIFO become empty... - yield from self.pulse(dut.read_en) - self.assertEqual((yield dut.empty), 1) - - # ... but we shouldn't see more space become available until we commit the read. - self.assertEqual((yield dut.space_available), 15) - yield from self.pulse(dut.read_commit) - self.assertEqual((yield dut.space_available), 16) - - # If we write 16 more bytes of data... - yield dut.write_en.eq(1) - for i in range(16): - yield dut.write_data.eq(i) - yield - yield dut.write_en.eq(0) - - # ... our buffer should be full, but also empty. - # This paradox exists as we've filled our buffer with uncomitted data. - yield - self.assertEqual((yield dut.full), 1) - self.assertEqual((yield dut.empty), 1) - - # Once we _commit_ our data, it should suddenly stop being empty. - yield from self.pulse(dut.write_commit) - self.assertEqual((yield dut.empty), 0) - - # Reading a byte _without committing_ shouldn't change anything about empty/full/space-available... - yield from self.pulse(dut.read_en) - self.assertEqual((yield dut.empty), 0) - self.assertEqual((yield dut.full), 1) - self.assertEqual((yield dut.space_available), 0) - - # ... but committing should increase our space available by one, and make our buffer no longer full. - yield from self.pulse(dut.read_commit) - self.assertEqual((yield dut.empty), 0) - self.assertEqual((yield dut.full), 0) - self.assertEqual((yield dut.space_available), 1) - - # Reading/committing another byte should increment our space available. - yield from self.pulse(dut.read_en) - yield from self.pulse(dut.read_commit) - self.assertEqual((yield dut.space_available), 2) - - # Writing data into the buffer should then fill it back up again... - yield dut.write_en.eq(1) - for i in range(2): - yield dut.write_data.eq(i) - yield - yield dut.write_en.eq(0) - - # ... meaning it will again be full, and have no space remaining. - yield - self.assertEqual((yield dut.full), 1) - self.assertEqual((yield dut.space_available), 0) - - # If we _discard_ this data, we should go back to having two bytes available. - yield from self.pulse(dut.write_discard) - self.assertEqual((yield dut.full), 0) - self.assertEqual((yield dut.space_available), 2) - - # If we read the data that's remaining in the fifo... - yield dut.read_en.eq(1) - for i in range(2, 16): - yield - self.assertEqual((yield dut.read_data), i) - yield dut.read_en.eq(0) - - # ... our buffer should again be empty. - yield - self.assertEqual((yield dut.empty), 1) - self.assertEqual((yield dut.space_available), 2) - - # If we _discard_ our current read, we should then see our buffer no longer empty... - yield from self.pulse(dut.read_discard) - self.assertEqual((yield dut.empty), 0) - - # and we should be able to read the same data again. - yield dut.read_en.eq(1) - for i in range(2, 16): - yield - self.assertEqual((yield dut.read_data), i) - yield dut.read_en.eq(0) - - # On committing this, we should see a buffer that is no longer full, and is really empty. - yield from self.pulse(dut.read_commit) - self.assertEqual((yield dut.empty), 1) - self.assertEqual((yield dut.full), 0) - self.assertEqual((yield dut.space_available), 16) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/stream/generator.py b/luna/gateware/stream/generator.py index 3ce26fc51..2da54f9cb 100644 --- a/luna/gateware/stream/generator.py +++ b/luna/gateware/stream/generator.py @@ -6,14 +6,8 @@ """ Stream generators. """ -import unittest - from amaranth import * from . import StreamInterface -from ..test import LunaUSBGatewareTestCase, LunaSSGatewareTestCase, ss_domain_test_case, usb_domain_test_case - -# Brought in for tests. -from ..usb.stream import SuperSpeedStreamInterface class ConstantStreamGenerator(Elaboratable): @@ -352,273 +346,6 @@ def elaborate(self, platform): return m -class ConstantStreamGeneratorTest(LunaUSBGatewareTestCase): - FRAGMENT_UNDER_TEST = ConstantStreamGenerator - FRAGMENT_ARGUMENTS = {'constant_data': b"HELLO, WORLD", 'domain': "usb", 'max_length_width': 16} - - @usb_domain_test_case - def test_basic_transmission(self): - dut = self.dut - - # Establish a very high max length; so it doesn't apply. - yield dut.max_length.eq(1000) - - # We shouldn't see a transmission before we request a start. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 0) - self.assertEqual((yield dut.stream.first), 0) - self.assertEqual((yield dut.stream.last), 0) - - # Once we pulse start, we should see the transmission start, - # and we should see our first byte of data. - yield from self.pulse(dut.start) - self.assertEqual((yield dut.stream.valid), 1) - self.assertEqual((yield dut.stream.payload), ord('H')) - self.assertEqual((yield dut.stream.first), 1) - - # That data should remain there until we accept it. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 1) - self.assertEqual((yield dut.stream.payload), ord('H')) - - # Once we indicate that we're accepting data... - yield dut.stream.ready.eq(1) - yield - - # ... we should start seeing the remainder of our transmission. - for i in 'ELLO': - yield - self.assertEqual((yield dut.stream.payload), ord(i)) - self.assertEqual((yield dut.stream.first), 0) - - - # If we drop the 'accepted', we should still see the next byte... - yield dut.stream.ready.eq(0) - yield - self.assertEqual((yield dut.stream.payload), ord(',')) - - # ... but that byte shouldn't be accepted, so we should remain there. - yield - self.assertEqual((yield dut.stream.payload), ord(',')) - - # If we start accepting data again... - yield dut.stream.ready.eq(1) - yield - - # ... we should see the remainder of the stream. - for i in ' WORLD': - yield - self.assertEqual((yield dut.stream.payload), ord(i)) - - - # On the last byte of data, we should see last = 1. - self.assertEqual((yield dut.stream.last), 1) - - # After the last datum, we should see valid drop to '0'. - yield - self.assertEqual((yield dut.stream.valid), 0) - - @usb_domain_test_case - def test_basic_start_position(self): - dut = self.dut - - # Start at position 2 - yield dut.start_position.eq(2) - - # Establish a very high max length; so it doesn't apply. - yield dut.max_length.eq(1000) - - # We shouldn't see a transmission before we request a start. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 0) - self.assertEqual((yield dut.stream.first), 0) - self.assertEqual((yield dut.stream.last), 0) - - # Once we pulse start, we should see the transmission start, - # and we should see our first byte of data. - yield from self.pulse(dut.start) - self.assertEqual((yield dut.stream.valid), 1) - self.assertEqual((yield dut.stream.payload), ord('L')) - self.assertEqual((yield dut.stream.first), 1) - - # That data should remain there until we accept it. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 1) - self.assertEqual((yield dut.stream.payload), ord('L')) - - # Once we indicate that we're accepting data... - yield dut.stream.ready.eq(1) - yield - - # ... we should start seeing the remainder of our transmission. - for i in 'LO': - yield - self.assertEqual((yield dut.stream.payload), ord(i)) - self.assertEqual((yield dut.stream.first), 0) - - - # If we drop the 'accepted', we should still see the next byte... - yield dut.stream.ready.eq(0) - yield - self.assertEqual((yield dut.stream.payload), ord(',')) - - # ... but that byte shouldn't be accepted, so we should remain there. - yield - self.assertEqual((yield dut.stream.payload), ord(',')) - - # If we start accepting data again... - yield dut.stream.ready.eq(1) - yield - - # ... we should see the remainder of the stream. - for i in ' WORLD': - yield - self.assertEqual((yield dut.stream.payload), ord(i)) - - - # On the last byte of data, we should see last = 1. - self.assertEqual((yield dut.stream.last), 1) - - # After the last datum, we should see valid drop to '0'. - yield - self.assertEqual((yield dut.stream.valid), 0) - - @usb_domain_test_case - def test_max_length(self): - dut = self.dut - - yield dut.stream.ready.eq(1) - yield dut.max_length.eq(6) - - # Once we pulse start, we should see the transmission start, - yield from self.pulse(dut.start) - - # ... we should start seeing the remainder of our transmission. - for i in 'HELLO': - self.assertEqual((yield dut.stream.payload), ord(i)) - yield - - - # On the last byte of data, we should see last = 1. - self.assertEqual((yield dut.stream.last), 1) - - # After the last datum, we should see valid drop to '0'. - yield - self.assertEqual((yield dut.stream.valid), 0) - - - -class ConstantStreamGeneratorWideTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = ConstantStreamGenerator - FRAGMENT_ARGUMENTS = dict( - domain = "ss", - constant_data = b"HELLO WORLD", - stream_type = SuperSpeedStreamInterface, - max_length_width = 16 - ) - - - @ss_domain_test_case - def test_basic_transmission(self): - dut = self.dut - - # Establish a very high max length; so it doesn't apply. - yield dut.max_length.eq(1000) - - # We shouldn't see a transmission before we request a start. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 0) - self.assertEqual((yield dut.stream.first), 0) - self.assertEqual((yield dut.stream.last), 0) - - # Once we pulse start, we should see the transmission start, - # and we should see our first byte of data. - yield from self.pulse(dut.start) - self.assertEqual((yield dut.stream.valid), 0b1111) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) - self.assertEqual((yield dut.stream.first), 1) - - # That data should remain there until we accept it. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.stream.valid), 0b1111) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) - - # Once we indicate that we're accepting data... - yield dut.stream.ready.eq(1) - yield - - # ... we should start seeing the remainder of our transmission. - yield - self.assertEqual((yield dut.stream.valid), 0b1111) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"O WO", byteorder="little")) - self.assertEqual((yield dut.stream.first), 0) - - - yield - self.assertEqual((yield dut.stream.valid), 0b111) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"RLD", byteorder="little")) - self.assertEqual((yield dut.stream.first), 0) - - - # On the last byte of data, we should see last = 1. - self.assertEqual((yield dut.stream.last), 1) - - # After the last datum, we should see valid drop to '0'. - yield - self.assertEqual((yield dut.stream.valid), 0) - - - @ss_domain_test_case - def test_max_length_transmission(self): - dut = self.dut - - # Apply a maximum length of six bytes. - yield dut.max_length.eq(6) - yield dut.stream.ready.eq(1) - - # Once we pulse start, we should see the transmission start, - # and we should see our first byte of data. - yield from self.pulse(dut.start) - self.assertEqual((yield dut.stream.valid), 0b1111) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) - self.assertEqual((yield dut.stream.first), 1) - - # We should then see only two bytes of our remainder. - yield - self.assertEqual((yield dut.stream.valid), 0b0011) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"O WO", byteorder="little")) - self.assertEqual((yield dut.stream.first), 0) - self.assertEqual((yield dut.stream.last), 1) - - - @ss_domain_test_case - def test_very_short_max_length(self): - dut = self.dut - - # Apply a maximum length of six bytes. - yield dut.max_length.eq(2) - - # Once we pulse start, we should see the transmission start, - # and we should see our first word of data. - yield from self.pulse(dut.start) - self.assertEqual((yield dut.stream.valid), 0b0011) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) - self.assertEqual((yield dut.stream.first), 1) - self.assertEqual((yield dut.stream.last), 1) - - # Our data should remain there until it's accepted. - yield dut.stream.ready.eq(1) - yield - self.assertEqual((yield dut.stream.valid), 0b0011) - self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) - self.assertEqual((yield dut.stream.first), 1) - self.assertEqual((yield dut.stream.last), 1) - - # After acceptance, valid should drop back to false. - yield - self.assertEqual((yield dut.stream.valid), 0b0000) - - class StreamSerializer(Elaboratable): """ Gateware that serializes a short Array input onto a stream. @@ -750,7 +477,3 @@ def elaborate(self, platform): m = DomainRenamer({"sync": self.domain})(m) return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/request/control.py b/luna/gateware/usb/request/control.py index 18f3f3ecb..e0ecff175 100644 --- a/luna/gateware/usb/request/control.py +++ b/luna/gateware/usb/request/control.py @@ -5,8 +5,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Full-gateware control request handlers. """ -import unittest - from amaranth import * from ..usb2.request import USBRequestHandler @@ -71,7 +69,3 @@ def handle_simple_data_request(self, m, transmitter, data, length=1): with m.If(self.interface.status_requested): m.d.comb += self.interface.handshakes_out.ack.eq(1) m.next = 'IDLE' - - -if __name__ == "__main__": - unittest.main(warnings="ignore") diff --git a/luna/gateware/usb/request/standard.py b/luna/gateware/usb/request/standard.py index 5cec86570..aaf3d50e1 100644 --- a/luna/gateware/usb/request/standard.py +++ b/luna/gateware/usb/request/standard.py @@ -7,7 +7,6 @@ import os import operator -import unittest import functools from typing import Iterable, Callable @@ -232,7 +231,3 @@ def elaborate(self, platform): m.next = 'IDLE' return m - - -if __name__ == "__main__": - unittest.main(warnings="ignore") diff --git a/luna/gateware/usb/stream.py b/luna/gateware/usb/stream.py index 962206942..7c40805fc 100644 --- a/luna/gateware/usb/stream.py +++ b/luna/gateware/usb/stream.py @@ -6,15 +6,12 @@ """ Core stream definitions. """ -import unittest - from amaranth import Elaboratable, Signal, Module from amaranth.hdl.rec import Record, DIR_FANIN, DIR_FANOUT from amaranth.hdl.xfrm import DomainRenamer from ..stream import StreamInterface from ..stream.arbiter import StreamArbiter -from ..test import LunaUSBGatewareTestCase, usb_domain_test_case class USBInStreamInterface(StreamInterface): @@ -261,65 +258,6 @@ def elaborate(self, platform): return m -class USBOutStreamBoundaryDetectorTest(LunaUSBGatewareTestCase): - FRAGMENT_UNDER_TEST = USBOutStreamBoundaryDetector - - @usb_domain_test_case - def test_boundary_detection(self): - dut = self.dut - processed_stream = self.dut.processed_stream - unprocesesed_stream = self.dut.unprocessed_stream - - # Before we see any data, we should have all of our strobes de-asserted, and an invalid stream. - self.assertEqual((yield processed_stream.valid), 0) - self.assertEqual((yield processed_stream.next), 0) - self.assertEqual((yield dut.first), 0) - self.assertEqual((yield dut.last), 0) - - # If our stream goes valid... - yield unprocesesed_stream.valid.eq(1) - yield unprocesesed_stream.next.eq(1) - yield unprocesesed_stream.payload.eq(0xAA) - yield - - # ... we shouldn't see anything this first cycle... - self.assertEqual((yield processed_stream.valid), 0) - self.assertEqual((yield processed_stream.next), 0) - self.assertEqual((yield dut.first), 0) - self.assertEqual((yield dut.last), 0) - - # ... but after two cycles... - yield unprocesesed_stream.payload.eq(0xBB) - yield - yield unprocesesed_stream.payload.eq(0xCC) - yield - - # ... we should see a valid stream's first byte. - self.assertEqual((yield processed_stream.valid), 1) - self.assertEqual((yield processed_stream.next), 1) - self.assertEqual((yield processed_stream.payload), 0xAA) - self.assertEqual((yield dut.first), 1) - self.assertEqual((yield dut.last), 0) - yield unprocesesed_stream.payload.eq(0xDD) - - # ... followed by a byte that's neither first nor last... - yield - self.assertEqual((yield processed_stream.payload), 0xBB) - self.assertEqual((yield dut.first), 0) - self.assertEqual((yield dut.last), 0) - - # Once our stream is no longer valid... - yield unprocesesed_stream.valid.eq(0) - yield unprocesesed_stream.next.eq(0) - yield - yield - - # ... we should see our final byte. - self.assertEqual((yield processed_stream.payload), 0xDD) - self.assertEqual((yield dut.first), 0) - self.assertEqual((yield dut.last), 1) - - class USBRawSuperSpeedStream(StreamInterface): """ Variant of LUNA's StreamInterface optimized for carrying raw USB3 data. @@ -393,9 +331,3 @@ class SuperSpeedStreamInterface(StreamInterface): def __init__(self): super().__init__(payload_width=32, valid_width=4) - - - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/control.py b/luna/gateware/usb/usb2/control.py index 7df6c14a7..8b23dfe88 100644 --- a/luna/gateware/usb/usb2/control.py +++ b/luna/gateware/usb/usb2/control.py @@ -5,8 +5,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Low-level USB transciever gateware -- control transfer components. """ -import unittest - from amaranth import Signal, Module, Elaboratable from usb_protocol.emitters import DeviceDescriptorCollection from usb_protocol.types import USBRequestType @@ -288,7 +286,3 @@ def elaborate(self, platform): m.d.comb += interface.handshakes_out.ack.eq(1) return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/descriptor.py b/luna/gateware/usb/usb2/descriptor.py index 5c7ccd912..d6b0664d0 100644 --- a/luna/gateware/usb/usb2/descriptor.py +++ b/luna/gateware/usb/usb2/descriptor.py @@ -6,7 +6,6 @@ """ Utilities for building USB descriptors into gateware. """ import struct -import unittest import functools from amaranth import * @@ -15,7 +14,6 @@ from ..stream import USBInStreamInterface from ...stream.generator import ConstantStreamGenerator -from ...test import LunaUSBGatewareTestCase, usb_domain_test_case from ...utils.bus import OneHotMultiplexer class USBDescriptorStreamGenerator(ConstantStreamGenerator): @@ -559,160 +557,6 @@ def elaborate(self, platform) -> Module: return m -class GetDescriptorHandlerBlockTest(LunaUSBGatewareTestCase): - descriptors = DeviceDescriptorCollection() - - with descriptors.DeviceDescriptor() as d: - d.bcdUSB = 2.00 - d.idVendor = 0x1234 - d.idProduct = 0x4567 - d.iManufacturer = "Manufacturer" - d.iProduct = "Product" - d.iSerialNumber = "ThisSerialNumberIsResultsInADescriptorLongerThan64Bytes" - d.bNumConfigurations = 1 - - with descriptors.ConfigurationDescriptor() as c: - c.bmAttributes = 0xC0 - c.bMaxPower = 50 - - with c.InterfaceDescriptor() as i: - i.bInterfaceNumber = 0 - i.bInterfaceClass = 0x02 - i.bInterfaceSubclass = 0x02 - i.bInterfaceProtocol = 0x01 - - with i.EndpointDescriptor() as e: - e.bEndpointAddress = 0x81 - e.bmAttributes = 0x03 - e.wMaxPacketSize = 64 - e.bInterval = 11 - - # HID Descriptor (Example E.8 of HID specification) - descriptors.add_descriptor(b'\x09\x21\x01\x01\x00\x01\x22\x00\x32') - - FRAGMENT_UNDER_TEST = GetDescriptorHandlerBlock - FRAGMENT_ARGUMENTS = {"descriptor_collection": descriptors} - - def traces_of_interest(self): - dut = self.dut - return (dut.value, dut.length, dut.start_position, dut.start, dut.stall, - dut.tx.ready, dut.tx.first, dut.tx.last, dut.tx.payload, dut.tx.valid) - - def _test_descriptor(self, type_number, index, raw_descriptor, start_position, max_length, delay_ready=0): - """ Triggers a read and checks if correct data is transmitted. """ - - # Set a defined start before starting - yield self.dut.tx.ready.eq(0) - yield - - # Set up request - yield self.dut.value.word_select(1, 8).eq(type_number) # Type - yield self.dut.value.word_select(0, 8).eq(index) # Index - yield self.dut.length.eq(max_length) - yield self.dut.start_position.eq(start_position) - yield self.dut.tx.ready.eq(1 if delay_ready == 0 else 0) - yield self.dut.start.eq(1) - yield - - yield self.dut.start.eq(0) - - yield from self.wait_until(self.dut.tx.valid, timeout=100) - - if delay_ready > 0: - for _ in range(delay_ready-1): - yield - yield self.dut.tx.ready.eq(1) - yield - - max_packet_length = 64 - expected_data = raw_descriptor[start_position:] - expected_bytes = min(len(expected_data), max_length-start_position, max_packet_length) - - if expected_bytes == 0: - self.assertEqual((yield self.dut.tx.first), 0) - self.assertEqual((yield self.dut.tx.last), 1) - self.assertEqual((yield self.dut.tx.valid), 1) - self.assertEqual((yield self.dut.stall), 0) - yield - - else: - for i in range(expected_bytes): - self.assertEqual((yield self.dut.tx.first), 1 if (i == 0) else 0) - self.assertEqual((yield self.dut.tx.last), 1 if (i == expected_bytes - 1) else 0) - self.assertEqual((yield self.dut.tx.valid), 1) - self.assertEqual((yield self.dut.tx.payload), expected_data[i]) - self.assertEqual((yield self.dut.stall), 0) - yield - - self.assertEqual((yield self.dut.tx.valid), 0) - - def _test_stall(self, type_number, index, start_position, max_length): - """ Triggers a read and checks if correctly stalled. """ - - yield self.dut.value.word_select(1, 8).eq(type_number) # Type - yield self.dut.value.word_select(0, 8).eq(index) # Index - yield self.dut.length.eq(max_length) - yield self.dut.start_position.eq(start_position) - yield self.dut.tx.ready.eq(1) - yield self.dut.start.eq(1) - yield - - yield self.dut.start.eq(0) - - cycles_passed = 0 - timeout = 100 - - while not (yield self.dut.stall): - self.assertEqual((yield self.dut.tx.valid), 0) - yield - - cycles_passed += 1 - if timeout and cycles_passed > timeout: - raise RuntimeError(f"Timeout waiting for stall!") - - @usb_domain_test_case - def test_all_descriptors(self): - for type_number, index, raw_descriptor in self.descriptors: - yield from self._test_descriptor(type_number, index, raw_descriptor, 0, len(raw_descriptor)) - yield from self._test_descriptor(type_number, index, raw_descriptor, 0, len(raw_descriptor), delay_ready=10) - - @usb_domain_test_case - def test_all_descriptors_with_offset(self): - for type_number, index, raw_descriptor in self.descriptors: - if len(raw_descriptor) > 1: - yield from self._test_descriptor(type_number, index, raw_descriptor, 1, len(raw_descriptor)) - - @usb_domain_test_case - def test_all_descriptors_with_length(self): - for type_number, index, raw_descriptor in self.descriptors: - if len(raw_descriptor) > 1: - yield from self._test_descriptor(type_number, index, raw_descriptor, 0, min(8, len(raw_descriptor)-1)) - yield from self._test_descriptor(type_number, index, raw_descriptor, 0, min(8, len(raw_descriptor)-1), delay_ready=10) - - @usb_domain_test_case - def test_all_descriptors_with_offset_and_length(self): - for type_number, index, raw_descriptor in self.descriptors: - if len(raw_descriptor) > 1: - yield from self._test_descriptor(type_number, index, raw_descriptor, 1, min(8, len(raw_descriptor)-1)) - - @usb_domain_test_case - def test_all_descriptors_with_zero_length(self): - for type_number, index, raw_descriptor in self.descriptors: - yield from self._test_descriptor(type_number, index, raw_descriptor, 0, 0) - - @usb_domain_test_case - def test_unavailable_descriptor(self): - yield from self._test_stall(StandardDescriptorNumbers.STRING, 100, 0, 64) - - @usb_domain_test_case - def test_unavailable_index_type(self): - # Unavailable index in between - yield from self._test_stall(0x10, 0, 0, 64) - - # Index after last used type - yield from self._test_stall(0x42, 0, 0, 64) - - class GetDescriptorHandlerMux(Elaboratable): def __init__(self, domain="usb"): self._domain = domain @@ -772,6 +616,3 @@ def elaborate(self, platform): m = DomainRenamer({"sync": self._domain})(m) return m - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/device.py b/luna/gateware/usb/usb2/device.py index c9e63d220..36ed8a84e 100644 --- a/luna/gateware/usb/usb2/device.py +++ b/luna/gateware/usb/usb2/device.py @@ -9,19 +9,16 @@ """ import logging -import unittest from luna import configure_default_logging from amaranth import Signal, Module, Elaboratable, Const -from usb_protocol.types import DescriptorTypes from usb_protocol.emitters import DeviceDescriptorCollection from ...interface.ulpi import UTMITranslator from ...interface.utmi import UTMIInterfaceMultiplexer from ...interface.gateware_phy import GatewarePHY -from . import USBSpeed, USBPacketID from .packet import USBTokenDetector, USBHandshakeGenerator, USBDataPacketCRC from .packet import USBInterpacketTimer, USBDataPacketGenerator, USBHandshakeDetector from .packet import USBDataPacketReceiver @@ -30,9 +27,6 @@ from .endpoint import USBEndpointMultiplexer from .control import USBControlEndpoint -from ...test import usb_domain_test_case -from ...test.usb2 import USBDeviceTest - class USBDevice(Elaboratable): """ Core gateware common to all LUNA USB2 devices. @@ -434,214 +428,3 @@ def elaborate(self, platform): ] return m - - -class FullDeviceTest(USBDeviceTest): - """ :meta private: """ - - FRAGMENT_UNDER_TEST = USBDevice - FRAGMENT_ARGUMENTS = {'handle_clocking': False} - - def traces_of_interest(self): - return ( - self.utmi.tx_data, - self.utmi.tx_valid, - self.utmi.rx_data, - self.utmi.rx_valid, - ) - - def initialize_signals(self): - - # Keep our device from resetting. - yield self.utmi.line_state.eq(0b01) - - # Have our USB device connected. - yield self.dut.connect.eq(1) - - # Pretend our PHY is always ready to accept data, - # so we can move forward quickly. - yield self.utmi.tx_ready.eq(1) - - - def provision_dut(self, dut): - self.descriptors = descriptors = DeviceDescriptorCollection() - - with descriptors.DeviceDescriptor() as d: - d.idVendor = 0x16d0 - d.idProduct = 0xf3b - - d.iManufacturer = "LUNA" - d.iProduct = "Test Device" - d.iSerialNumber = "1234" - - d.bNumConfigurations = 1 - - # Provide a core configuration descriptor for testing. - with descriptors.ConfigurationDescriptor() as c: - - with c.InterfaceDescriptor() as i: - i.bInterfaceNumber = 0 - - with i.EndpointDescriptor() as e: - e.bEndpointAddress = 0x01 - e.wMaxPacketSize = 512 - - with i.EndpointDescriptor() as e: - e.bEndpointAddress = 0x81 - e.wMaxPacketSize = 512 - - dut.add_standard_control_endpoint(descriptors) - - - @usb_domain_test_case - def test_enumeration(self): - - # Reference enumeration process (quirks merged from Linux, macOS, and Windows): - # - Read 8 bytes of device descriptor. - # - Read 64 bytes of device descriptor. - # - Set address. - # - Read exact device descriptor length. - # - Read device qualifier descriptor, three times. - # - Read config descriptor (without subordinates). - # - Read language descriptor. - # - Read Windows extended descriptors. [optional] - # - Read string descriptors from device descriptor (wIndex=language id). - # - Set configuration. - # - Read back configuration number and validate. - - - # Read 8 bytes of our device descriptor. - handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=8) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)[0:8]) - - # Read 64 bytes of our device descriptor, no matter its length. - handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=64) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)) - - # Send a nonsense request, and validate that it's stalled. - handshake, data = yield from self.control_request_in(0x80, 30, length=10) - self.assertEqual(handshake, USBPacketID.STALL) - - # Send a set-address request; we'll apply an arbitrary address 0x31. - yield from self.set_address(0x31) - self.assertEqual(self.address, 0x31) - - # Read our device descriptor. - handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=18) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)) - - # Read our device qualifier descriptor. - for _ in range(3): - handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE_QUALIFIER, length=10) - self.assertEqual(handshake, USBPacketID.STALL) - - # Read our configuration descriptor (no subordinates). - handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=9) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION)[0:9]) - - # Read our configuration descriptor (with subordinates). - handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=32) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION)) - - # Read our string descriptors. - for i in range(4): - handshake, data = yield from self.get_descriptor(DescriptorTypes.STRING, index=i, length=255) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.STRING, index=i)) - - # Set our configuration... - status_pid = yield from self.set_configuration(1) - self.assertEqual(status_pid, USBPacketID.DATA1) - - # ... and ensure it's applied. - handshake, configuration = yield from self.get_configuration() - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(configuration, [1], "device did not accept configuration!") - - -class LongDescriptorTest(USBDeviceTest): - """ :meta private: """ - - FRAGMENT_UNDER_TEST = USBDevice - FRAGMENT_ARGUMENTS = {'handle_clocking': False} - - def initialize_signals(self): - - # Keep our device from resetting. - yield self.utmi.line_state.eq(0b01) - - # Have our USB device connected. - yield self.dut.connect.eq(1) - - # Pretend our PHY is always ready to accept data, - # so we can move forward quickly. - yield self.utmi.tx_ready.eq(1) - - - def provision_dut(self, dut): - self.descriptors = descriptors = DeviceDescriptorCollection() - - with descriptors.DeviceDescriptor() as d: - d.idVendor = 0x16d0 - d.idProduct = 0xf3b - - d.iManufacturer = "LUNA" - d.iProduct = "Test Device" - d.iSerialNumber = "1234" - - d.bNumConfigurations = 1 - - # Provide a core configuration descriptor for testing. - with descriptors.ConfigurationDescriptor() as c: - - with c.InterfaceDescriptor() as i: - i.bInterfaceNumber = 0 - - for n in range(15): - - with i.EndpointDescriptor() as e: - e.bEndpointAddress = n - e.wMaxPacketSize = 512 - - with i.EndpointDescriptor() as e: - e.bEndpointAddress = 0x80 | n - e.wMaxPacketSize = 512 - - dut.add_standard_control_endpoint(descriptors) - - @usb_domain_test_case - def test_long_descriptor(self): - descriptor = self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION) - - # Read our configuration descriptor (no subordinates). - handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=len(descriptor)) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), descriptor) - self.assertEqual(len(data), len(descriptor)) - - @usb_domain_test_case - def test_descriptor_zlp(self): - # Try requesting a long descriptor, but using a length that is a - # multiple of the endpoint's maximum packet length. This should cause - # the device to return some number of packets with the maximum packet - # length, followed by a zero-length packet to terminate the - # transaction. - - descriptor = self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION) - - # Try requesting a single and three max-sized packet. - for factor in [1, 3]: - request_length = self.max_packet_size_ep0 * factor - handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=request_length) - self.assertEqual(handshake, USBPacketID.ACK) - self.assertEqual(bytes(data), descriptor[0:request_length]) - self.assertEqual(len(data), request_length) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/packet.py b/luna/gateware/usb/usb2/packet.py index f5a23bd3b..a8f61f70d 100644 --- a/luna/gateware/usb/usb2/packet.py +++ b/luna/gateware/usb/usb2/packet.py @@ -8,7 +8,6 @@ import operator -import unittest import functools from amaranth import Signal, Module, Elaboratable, Cat, Array, Const @@ -17,7 +16,6 @@ from . import USBSpeed, USBPacketID from ..stream import USBInStreamInterface, USBOutStreamInterface from ...interface.utmi import UTMITransmitInterface -from ...test import LunaGatewareTestCase, usb_domain_test_case # # Interfaces. @@ -200,59 +198,6 @@ def attach(self, *subordinates): # Gateware. # - -class USBPacketizerTest(LunaGatewareTestCase): - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - def instantiate_dut(self, extra_arguments=None): - self.utmi = Record([ - ("rx_data", 8), - ("rx_active", 1), - ("rx_valid", 1) - ]) - - # If we don't have explicit extra arguments, use the base class's. - if extra_arguments is None: - extra_arguments = self.FRAGMENT_ARGUMENTS - - return self.FRAGMENT_UNDER_TEST(utmi=self.utmi, **extra_arguments) - - def provide_byte(self, byte): - """ Provides a given byte on the UTMI receive data for one cycle. """ - yield self.utmi.rx_data.eq(byte) - yield - - - def start_packet(self, *, set_rx_valid=True): - """ Starts a UTMI packet receive. """ - yield self.utmi.rx_active.eq(1) - - if set_rx_valid: - yield self.utmi.rx_valid.eq(1) - - yield - - - def end_packet(self): - """ Starts a UTMI packet receive. """ - yield self.utmi.rx_active.eq(0) - yield self.utmi.rx_valid.eq(0) - yield - - - def provide_packet(self, *octets, cycle_after=True): - """ Provides an entire packet transaction at once; for convenience. """ - yield from self.start_packet() - for b in octets: - yield from self.provide_byte(b) - yield from self.end_packet() - - if cycle_after: - yield - - - class USBTokenDetector(Elaboratable): """ Gateware that parses token packets and generates relevant events. @@ -469,69 +414,6 @@ def elaborate(self, platform): return m -class USBTokenDetectorTest(USBPacketizerTest): - FRAGMENT_UNDER_TEST = USBTokenDetector - - @usb_domain_test_case - def test_valid_token(self): - dut = self.dut - - # Assume our device is at address 0x3a. - yield dut.address.eq(0x3a) - - # When idle, we should have no new-packet events. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.interface.new_frame), 0) - self.assertEqual((yield dut.interface.new_token), 0) - - # From: https://usb.org/sites/default/files/crcdes.pdf - # out to 0x3a, endpoint 0xa => 0xE1 5C BC - yield from self.provide_packet(0b11100001, 0b00111010, 0b00111101) - - # Validate that we just finished a token. - self.assertEqual((yield dut.interface.new_token), 1) - self.assertEqual((yield dut.interface.new_frame), 0) - - # Validate that we got the expected PID. - self.assertEqual((yield dut.interface.pid), 0b0001) - - # Validate that we got the expected address / endpoint. - self.assertEqual((yield dut.interface.address), 0x3a) - self.assertEqual((yield dut.interface.endpoint), 0xa ) - - # Ensure that our strobe returns to 0, afterwards. - yield - self.assertEqual((yield dut.interface.new_token), 0) - - - @usb_domain_test_case - def test_valid_start_of_frame(self): - dut = self.dut - yield from self.provide_packet(0b10100101, 0b00111010, 0b00111101) - - # Validate that we just finished a token. - self.assertEqual((yield dut.interface.new_token), 0) - self.assertEqual((yield dut.interface.new_frame), 1) - - # Validate that we got the expected address / endpoint. - self.assertEqual((yield dut.interface.frame), 0x53a) - - - @usb_domain_test_case - def test_token_to_other_device(self): - dut = self.dut - - # Assume our device is at 0x1f. - yield dut.address.eq(0x1f) - - # From: https://usb.org/sites/default/files/crcdes.pdf - # out to 0x3a, endpoint 0xa => 0xE1 5C BC - yield from self.provide_packet(0b11100001, 0b00111010, 0b00111101) - - # Validate that we did not count this as a token received, - # as it wasn't for us. - self.assertEqual((yield dut.interface.new_token), 0) - class USBHandshakeDetector(Elaboratable): """ Gateware that detects handshake packets. @@ -632,29 +514,6 @@ def elaborate(self, platform): return m -class USBHandshakeDetectorTest(USBPacketizerTest): - FRAGMENT_UNDER_TEST = USBHandshakeDetector - - @usb_domain_test_case - def test_ack(self): - yield from self.provide_packet(0b11010010) - self.assertEqual((yield self.dut.detected.ack), 1) - - @usb_domain_test_case - def test_nak(self): - yield from self.provide_packet(0b01011010) - self.assertEqual((yield self.dut.detected.nak), 1) - - @usb_domain_test_case - def test_stall(self): - yield from self.provide_packet(0b00011110) - self.assertEqual((yield self.dut.detected.stall), 1) - - @usb_domain_test_case - def test_nyet(self): - yield from self.provide_packet(0b10010110) - self.assertEqual((yield self.dut.detected.nyet), 1) - class USBDataPacketCRC(Elaboratable): @@ -1040,101 +899,6 @@ def elaborate(self, platform): return m -class USBDataPacketReceiverTest(USBPacketizerTest): - FRAGMENT_UNDER_TEST = USBDataPacketReceiver - FRAGMENT_ARGUMENTS = {'standalone': True} - - @usb_domain_test_case - def test_data_receive(self): - dut = self.dut - stream = self.dut.stream - - - # 0xC3, # PID: Data - # 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA - # 0xEB, 0xBC # CRC - - # Before we send anything, our stream should be inactive. - self.assertEqual((yield stream.valid), 0) - - # After sending our PID, we shouldn't see our stream be active. - yield from self.start_packet() - yield from self.provide_byte(0xc3) - self.assertEqual((yield stream.valid), 0) - - # The stream shouldn't go valid for our first two data bytes, either. - for b in [0x00, 0x05]: - yield from self.provide_byte(b) - self.assertEqual((yield stream.valid), 0) - - # Check that our active PID was successfully captured, and our current on - # hasn't yet been updated. - self.assertEqual((yield self.dut.active_pid), 3) - self.assertEqual((yield self.dut.packet_id), 0) - - # The third byte should finally trigger our stream output... - yield from self.provide_byte(0x08) - self.assertEqual((yield stream.valid), 1) - - # ... and we should see the first byte on our stream. - self.assertEqual((yield stream.next), 1) - self.assertEqual((yield stream.payload), 0x00) - - # If we pause RxValid, we nothing should advance. - yield self.utmi.rx_valid.eq(0) - - yield from self.provide_byte(0x08) - self.assertEqual((yield stream.next), 0) - self.assertEqual((yield stream.payload), 0x00) - - # Resuming should continue our advance... - yield self.utmi.rx_valid.eq(1) - - # ... and we should process the remainder of the input - for b in [0x00, 0x00, 0x00, 0x00, 0x00, 0xEB, 0xBC]: - yield from self.provide_byte(b) - - # ... remaining two bytes behind. - self.assertEqual((yield stream.next), 1) - self.assertEqual((yield stream.payload), 0x00) - - - # When we stop our packet, we should see our stream stop as well. - # The last two bytes, our CRC, shouldn't be included. - yield from self.end_packet() - yield - - # And, since we sent a valid packet, we should see a pulse indicating the packet is valid. - self.assertEqual((yield stream.valid), 0) - self.assertEqual((yield self.dut.packet_complete), 1) - - # After an inter-packet delay, we should see that we're ready to respond. - yield from self.advance_cycles(9) - self.assertEqual((yield self.dut.ready_for_response), 0) - - - @usb_domain_test_case - def test_zlp(self): - dut = self.dut - stream = self.dut.stream - - # 0x4B # DATA1 - # 0x00, 0x00 # CRC - - # Send our data PID. - yield from self.start_packet() - yield from self.provide_byte(0x4B) - - # Send our CRC. - for b in [0x00, 0x00]: - yield from self.provide_byte(b) - self.assertEqual((yield stream.valid), 0) - - yield from self.end_packet() - yield - - self.assertEqual((yield self.dut.packet_complete), 1) - class USBDataPacketDeserializer(Elaboratable): """ Gateware that captures USB data packet contents and parallelizes them. @@ -1298,53 +1062,6 @@ def elaborate(self, platform): return m -class USBDataPacketDeserializerTest(USBPacketizerTest): - FRAGMENT_UNDER_TEST = USBDataPacketDeserializer - - def instantiate_dut(self): - return super().instantiate_dut(extra_arguments={'create_crc_generator': True}) - - - @usb_domain_test_case - def test_packet_rx(self): - yield from self.provide_packet( - 0b11000011, # PID - 0b00100011, 0b01000101, 0b01100111, 0b10001001, # DATA - 0b00001110, 0b00011100 # CRC - ) - - # Ensure we've gotten a new packet. - self.assertEqual((yield self.dut.new_packet), 1, "packet not recognized") - self.assertEqual((yield self.dut.length), 4) - self.assertEqual((yield self.dut.packet[0]), 0b00100011) - self.assertEqual((yield self.dut.packet[1]), 0b01000101) - self.assertEqual((yield self.dut.packet[2]), 0b01100111) - self.assertEqual((yield self.dut.packet[3]), 0b10001001) - - - @usb_domain_test_case - def test_captured_usb_sample(self): - yield from self.provide_packet( - 0xC3, # PID: Data - 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA - 0xEB, 0xBC # CRC - ) - - # Ensure we've gotten a new packet. - self.assertEqual((yield self.dut.new_packet), 1, "packet not recognized") - - @usb_domain_test_case - def test_invalid_rx(self): - yield from self.provide_packet( - 0b11000011, # PID - 0b11111111, 0b11111111, 0b11111111, 0b11111111, # DATA - 0b00011100, 0b00001110 # CRC - ) - - # Ensure we've gotten a new packet. - self.assertEqual((yield self.dut.new_packet), 0, 'accepted invalid CRC!') - - class USBDataPacketGenerator(Elaboratable): """ Module that converts a FIFO-style stream into a USB data packet. @@ -1520,149 +1237,6 @@ def elaborate(self, platform): return m -class USBDataPacketGeneratorTest(LunaGatewareTestCase): - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - FRAGMENT_UNDER_TEST = USBDataPacketGenerator - FRAGMENT_ARGUMENTS = {'standalone': True} - - def initialize_signals(self): - # Model our PHY is always accepting data, by default. - yield self.dut.tx.ready.eq(1) - - - @usb_domain_test_case - def test_simple_data_generation(self): - dut = self.dut - stream = self.dut.stream - tx = self.dut.tx - - # We'll request that a simple USB packet be sent. We expect the following data: - # 0xC3, # PID: Data - # 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA - # 0xEB, 0xBC # CRC - - # Before we send anything, we shouldn't be transmitting. - self.assertEqual((yield dut.tx.valid), 0) - - # Work with DATA0 packet IDs. - yield dut.data_pid.eq(0) - - # Start sending our first byte. - yield stream.first.eq(1) - yield stream.valid.eq(1) - yield stream.payload.eq(0x00) - yield - - # Once our first byte has been provided, our transmission should - # start (valid=1), and we should see our data PID. - yield - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0xc3) - - # We shouldn't consume any data, yet, as we're still - # transmitting our PID. - self.assertEqual((yield stream.ready), 0) - - # Drop our first value back to zero, as it should also work as a strobe. - yield stream.first.eq(0) - - # One cycle later, we should see our first data byte, and our - # stream should indicate that data was consumed. - yield - self.assertEqual((yield tx.data), 0x00) - self.assertEqual((yield stream.ready), 1) - - # Provide the remainder of our data, and make sure that our - # output value mirrors it. - for datum in [0x05, 0x08, 0x00, 0x00, 0x00, 0x00]: - yield stream.payload.eq(datum) - yield - self.assertEqual((yield tx.data), datum) - - # Finally, provide our last data value. - yield stream.payload.eq(0x00) - yield stream.last.eq(1) - yield - - # Drop our stream-valid to zero after the last stream byte. - yield stream.valid.eq(0) - - # We should now see that we're no longer consuming data... - yield - self.assertEqual((yield stream.ready), 0) - - # ... but the transmission is still valid; and now presenting our CRC... - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0xeb) - - # ... which is two-bytes long. - yield - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0xbc) - - # Once our CRC is completed, our transmission should stop. - yield - self.assertEqual((yield tx.valid), 0) - - - @usb_domain_test_case - def test_single_byte(self): - stream = self.dut.stream - - # Request single byte. - yield stream.first.eq(1) - yield stream.last.eq(1) - yield stream.valid.eq(1) - yield stream.payload.eq(0xAB) - yield from self.wait_until(stream.ready) - - # Drop our last back to zero, immediately. - yield stream.last.eq(0) - yield stream.first.eq(0) - yield stream.valid.eq(0) - - yield from self.advance_cycles(10) - - - @usb_domain_test_case - def test_zlp_generation(self): - stream = self.dut.stream - tx = self.dut.tx - - # Request a ZLP. - yield stream.first.eq(0) - yield stream.last.eq(1) - yield stream.valid.eq(1) - yield - - # Drop our last back to zero, immediately. - yield stream.last.eq(0) - - # Once our first byte has been provided, our transmission should - # start (valid=1), and we should see our data PID. - yield - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0xc3) - - # Drop our stream-valid to zero after the last stream byte. - yield stream.valid.eq(0) - - # We should now see that we're no longer consuming data... - yield - self.assertEqual((yield stream.ready), 0) - - # ... but the transmission is still valid; and now presenting our CRC... - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0x0) - - # ... which is two-bytes long. - yield - self.assertEqual((yield tx.valid), 1) - self.assertEqual((yield tx.data), 0x0) - - class USBHandshakeGenerator(Elaboratable): """ Module that generates handshake packets, on request. @@ -1736,70 +1310,6 @@ def elaborate(self, platform): return m -class USBHandshakeGeneratorTest(LunaGatewareTestCase): - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - FRAGMENT_UNDER_TEST = USBHandshakeGenerator - - - @usb_domain_test_case - def test_ack_generation(self): - dut = self.dut - - # Before we request anything, our data shouldn't be valid. - self.assertEqual((yield dut.tx.valid), 0) - - # When we request an ACK... - yield dut.issue_ack.eq(1) - yield - yield dut.issue_ack.eq(0) - - # ... we should see an ACK packet on our data lines... - yield - self.assertEqual((yield dut.tx.data), USBHandshakeGenerator._PACKET_ACK) - - # ... our transmit request should be valid. - self.assertEqual((yield dut.tx.valid), 1) - - # It should remain valid... - yield from self.advance_cycles(10) - self.assertEqual((yield dut.tx.valid), 1) - - # ... until the UTMI transceiver marks it as accepted... - yield dut.tx.ready.eq(1) - yield - - # ... when our packet should be marked as invalid. - yield - self.assertEqual((yield dut.tx.valid), 0) - - - @usb_domain_test_case - def test_already_ready(self): - dut = self.dut - - # Start off with our transmitter ready to receive. - yield dut.tx.ready.eq(1) - - # When we request an ACK... - yield dut.issue_ack.eq(1) - yield - yield dut.issue_ack.eq(0) - - # ... we should see an ACK packet on our data lines... - yield - self.assertEqual((yield dut.tx.data), USBHandshakeGenerator._PACKET_ACK) - - # ... our transmit request should be valid... - self.assertEqual((yield dut.tx.valid), 1) - - # ... and then drop out of being valid after one cycle. - yield - self.assertEqual((yield dut.tx.valid), 0) - - - class USBInterpacketTimer(Elaboratable): """ Module that tracks inter-packet timings, enforcing spec-mandated packet gaps. @@ -1951,64 +1461,3 @@ def elaborate(self, platform): return m - -class USBInterpacketTimerTest(LunaGatewareTestCase): - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - def instantiate_dut(self): - dut = USBInterpacketTimer() - - # Create our primary timer interface. - self.interface = InterpacketTimerInterface() - dut.add_interface(self.interface) - - return dut - - - def initialize_signals(self): - # Assume FS for our tests, unless overridden. - yield self.dut.speed(USBSpeed.FULL) - - - def test_resets_and_delays(self): - yield from self.advance_cycles(4) - interface = self.interface - - # Trigger a cycle reset. - yield interface.start.eq(1) - yield - yield interface.start.eq(0) - - # We should start off with no timer outputs high. - self.assertEqual((yield interface.tx_allowed), 0) - self.assertEqual((yield interface.tx_timeout), 0) - self.assertEqual((yield interface.rx_timeout), 0) - - # 10 cycles later, we should see our first timer output. - yield from self.advance_cycles(10) - self.assertEqual((yield interface.tx_allowed), 1) - self.assertEqual((yield interface.tx_timeout), 0) - self.assertEqual((yield interface.rx_timeout), 0) - - # 22 cycles later (32 total), we should see our second timer output. - yield from self.advance_cycles(22) - self.assertEqual((yield interface.tx_allowed), 0) - self.assertEqual((yield interface.tx_timeout), 1) - self.assertEqual((yield interface.rx_timeout), 0) - - # 58 cycles later (80 total), we should see our third timer output. - yield from self.advance_cycles(22) - self.assertEqual((yield interface.tx_allowed), 0) - self.assertEqual((yield interface.tx_timeout), 0) - self.assertEqual((yield interface.rx_timeout), 1) - - # Ensure that the timers don't go high again. - for _ in range(32): - self.assertEqual((yield self.rx_to_tx_min), 0) - self.assertEqual((yield self.rx_to_tx_max), 0) - self.assertEqual((yield self.tx_to_rx_timeout), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/request.py b/luna/gateware/usb/usb2/request.py index ef0740fb7..31af19d69 100644 --- a/luna/gateware/usb/usb2/request.py +++ b/luna/gateware/usb/usb2/request.py @@ -6,7 +6,6 @@ """ Low-level USB transciever gateware -- control request components. """ -import unittest import functools import operator @@ -15,15 +14,12 @@ from amaranth.hdl.rec import Record, DIR_FANOUT from . import USBSpeed -from .packet import USBTokenDetector, USBDataPacketDeserializer, USBPacketizerTest +from .packet import USBTokenDetector, USBDataPacketDeserializer from .packet import DataCRCInterface, USBInterpacketTimer, TokenDetectorInterface from .packet import InterpacketTimerInterface, HandshakeExchangeInterface from ..stream import USBInStreamInterface, USBOutStreamInterface from ..request import SetupPacket -from ...test import usb_domain_test_case - - class RequestHandlerInterface: """ Record representing a connection between a control endpoint and a request handler. @@ -271,112 +267,6 @@ def elaborate(self, platform): return m -class USBSetupDecoderTest(USBPacketizerTest): - FRAGMENT_UNDER_TEST = USBSetupDecoder - FRAGMENT_ARGUMENTS = {'standalone': True} - - - def initialize_signals(self): - - # Assume high speed. - yield self.dut.speed.eq(USBSpeed.HIGH) - - - def provide_reference_setup_transaction(self): - """ Provide a reference SETUP transaction. """ - - # Provide our setup packet. - yield from self.provide_packet( - 0b00101101, # PID: SETUP token. - 0b00000000, 0b00010000 # Address 0, endpoint 0, CRC - ) - - # Provide our data packet. - yield from self.provide_packet( - 0b11000011, # PID: DATA0 - 0b0_10_00010, # out vendor request to endpoint - 12, # request number 12 - 0xcd, 0xab, # value 0xABCD (little endian) - 0x23, 0x01, # index 0x0123 - 0x78, 0x56, # length 0x5678 - 0x3b, 0xa2, # CRC - ) - - - @usb_domain_test_case - def test_valid_sequence_receive(self): - dut = self.dut - - # Before we receive anything, we shouldn't have a new packet. - self.assertEqual((yield dut.packet.received), 0) - - # Simulate the host sending basic setup data. - yield from self.provide_reference_setup_transaction() - - # We're high speed, so we should be ACK'ing immediately. - self.assertEqual((yield dut.ack), 1) - - # We now should have received a new setup request. - yield - self.assertEqual((yield dut.packet.received), 1) - - # Validate that its values are as we expect. - self.assertEqual((yield dut.packet.is_in_request), 0 ) - self.assertEqual((yield dut.packet.type), 0b10 ) - self.assertEqual((yield dut.packet.recipient), 0b00010 ) - self.assertEqual((yield dut.packet.request), 12 ) - self.assertEqual((yield dut.packet.value), 0xabcd ) - self.assertEqual((yield dut.packet.index), 0x0123 ) - self.assertEqual((yield dut.packet.length), 0x5678 ) - - - @usb_domain_test_case - def test_fs_interpacket_delay(self): - dut = self.dut - - # Place our DUT into full speed mode. - yield dut.speed.eq(USBSpeed.FULL) - - # Before we receive anything, we shouldn't have a new packet. - self.assertEqual((yield dut.packet.received), 0) - - # Simulate the host sending basic setup data. - yield from self.provide_reference_setup_transaction() - - # We shouldn't ACK immediately; we'll need to wait our interpacket delay. - yield - self.assertEqual((yield dut.ack), 0) - - # After our minimum interpacket delay, we should see an ACK. - yield from self.advance_cycles(10) - self.assertEqual((yield dut.ack), 1) - - - - @usb_domain_test_case - def test_short_setup_packet(self): - dut = self.dut - - # Before we receive anything, we shouldn't have a new packet. - self.assertEqual((yield dut.packet.received), 0) - - # Provide our setup packet. - yield from self.provide_packet( - 0b00101101, # PID: SETUP token. - 0b00000000, 0b00010000 # Address 0, endpoint 0, CRC - ) - - # Provide our data packet; but shorter than expected. - yield from self.provide_packet( - 0b11000011, # PID: DATA0 - 0b00100011, 0b01000101, 0b01100111, 0b10001001, # DATA - 0b00011100, 0b00001110 # CRC - ) - - # This shouldn't count as a valid setup packet. - yield - self.assertEqual((yield dut.packet.received), 0) - class USBRequestHandlerMultiplexer(Elaboratable): """ Multiplexes multiple RequestHandlers down to a single interface. @@ -554,8 +444,3 @@ def elaborate(self, platform): m.d.comb += self.interface.handshakes_out.stall.eq(1) return m - - - -if __name__ == "__main__": - unittest.main(warnings="ignore") diff --git a/luna/gateware/usb/usb2/reset.py b/luna/gateware/usb/usb2/reset.py index 2b0fa5866..f39707ee0 100644 --- a/luna/gateware/usb/usb2/reset.py +++ b/luna/gateware/usb/usb2/reset.py @@ -6,13 +6,10 @@ """ Gateware that handles USB bus resets & speed detection. """ -import unittest - from amaranth import * from . import USBSpeed from ...interface.utmi import UTMITransmitInterface, UTMIOperatingMode, UTMITerminationSelect -from ...test import LunaGatewareTestCase, usb_domain_test_case def _generate_wide_incrementer(m, platform, adder_input): @@ -512,77 +509,3 @@ def elaborate(self, platform): m.next = 'START_HS_DETECTION' return m - - -class USBResetSequencerTest(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = USBResetSequencer - - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - def instantiate_dut(self): - dut = super().instantiate_dut() - - # Test tweak: squish down our delays to speed up sim. - dut._CYCLES_2P5_MICROSECONDS = 10 - - return dut - - - def initialize_signals(self): - - # Start with a non-reset line-state. - yield self.dut.line_state.eq(0b01) - - - @usb_domain_test_case - def test_full_speed_reset(self): - dut = self.dut - - yield from self.advance_cycles(10) - - # Before we detect a reset, we should be at normal FS, - # and we should be in reset until VBUS is provided. - self.assertEqual((yield dut.bus_reset), 1) - self.assertEqual((yield dut.current_speed), USBSpeed.FULL) - self.assertEqual((yield dut.operating_mode), UTMIOperatingMode.NORMAL) - self.assertEqual((yield dut.termination_select), UTMITerminationSelect.LS_FS_NORMAL) - - # Once we apply VBUS, we should drop out of reset... - yield dut.vbus_connected.eq(1) - yield - self.assertEqual((yield dut.bus_reset), 0) - - # ... and stay that way. - yield from self.advance_cycles(dut._CYCLES_2P5_MICROSECONDS) - self.assertEqual((yield dut.bus_reset), 0) - - yield dut.line_state.eq(0) - - # After assertion of SE0, we should remain out of reset for 2.5uS... - yield - self.assertEqual((yield dut.bus_reset), 0) - - # ... and then we should see a cycle of reset. - yield from self.advance_cycles(dut._CYCLES_2P5_MICROSECONDS + 1) - self.assertEqual((yield dut.bus_reset), 1) - - yield from self.advance_cycles(10) - yield dut.line_state.eq(0b01) - yield - - # Finally, we should arrive in FS, post-reset. - self.assertEqual((yield dut.current_speed), USBSpeed.FULL) - self.assertEqual((yield dut.operating_mode), UTMIOperatingMode.NORMAL) - self.assertEqual((yield dut.termination_select), UTMITerminationSelect.LS_FS_NORMAL) - - - # - # It would be lovely to have tests that run through each of our reset/suspend - # cases here; but currently the time it takes run through the relevant delays is - # prohibitive. :( - # - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb2/transfer.py b/luna/gateware/usb/usb2/transfer.py index 9dd5faba3..81feef54e 100644 --- a/luna/gateware/usb/usb2/transfer.py +++ b/luna/gateware/usb/usb2/transfer.py @@ -9,8 +9,6 @@ Its components facilitate data transfer longer than a single packet. """ -import unittest - from amaranth import Signal, Elaboratable, Module, Array from amaranth.hdl.mem import Memory @@ -18,8 +16,6 @@ from ..stream import USBInStreamInterface from ...stream import StreamInterface -from ...test import LunaGatewareTestCase, usb_domain_test_case - class USBInTransferManager(Elaboratable): """ Sequencer that converts a long data stream (a USB *transfer*) into a burst of USB packets. @@ -373,289 +369,3 @@ def elaborate(self, platform): m.next = 'WAIT_TO_SEND' return m - - -class USBInTransferManagerTest(LunaGatewareTestCase): - FRAGMENT_UNDER_TEST = USBInTransferManager - FRAGMENT_ARGUMENTS = {"max_packet_size": 8} - - SYNC_CLOCK_FREQUENCY = None - USB_CLOCK_FREQUENCY = 60e6 - - def initialize_signals(self): - - # By default, pretend our transmitter is always accepting data... - yield self.dut.packet_stream.ready.eq(1) - - # And pretend that our host is always tagreting our endpoint. - yield self.dut.active.eq(1) - yield self.dut.tokenizer.is_in.eq(1) - - - @usb_domain_test_case - def test_normal_transfer(self): - dut = self.dut - - packet_stream = dut.packet_stream - transfer_stream = dut.transfer_stream - - # Before we do anything, we shouldn't have anything our output stream. - self.assertEqual((yield packet_stream.valid), 0) - - # Our transfer stream should accept data until we fill up its buffers. - self.assertEqual((yield transfer_stream.ready), 1) - - # Once we start sending data to our packetizer... - yield transfer_stream.valid.eq(1) - yield transfer_stream.payload.eq(0x11) - yield - - # We still shouldn't see our packet stream start transmitting; - # and we should still be accepting data. - self.assertEqual((yield packet_stream.valid), 0) - self.assertEqual((yield transfer_stream.ready), 1) - - # Once we see a full packet... - for value in [0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - yield transfer_stream.payload.eq(value) - yield - yield transfer_stream.valid.eq(0) - - # ... we shouldn't see a transmit request until we receive an IN token. - self.assertEqual((yield transfer_stream.ready), 1) - yield from self.advance_cycles(5) - self.assertEqual((yield packet_stream.valid), 0) - - # We -should-, however, keep filling our secondary buffer while waiting. - yield transfer_stream.valid.eq(1) - self.assertEqual((yield transfer_stream.ready), 1) - for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: - yield transfer_stream.payload.eq(value) - yield - - # Once we've filled up -both- buffers, our data should no longer be ready. - yield - self.assertEqual((yield transfer_stream.ready), 0) - - # Once we do see an IN token... - yield from self.pulse(dut.tokenizer.ready_for_response) - - # ... we should start transmitting... - self.assertEqual((yield packet_stream.valid), 1) - - # ... we should see the full packet be emitted... - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - - # ... and then the packet should end. - self.assertEqual((yield packet_stream.valid), 0) - - # We should now be waiting for an ACK. While waiting, we still need - # to keep the last packet; so we'll expect that we're not ready for data. - self.assertEqual((yield transfer_stream.ready), 0) - - # If we receive anything other than an ACK... - yield from self.pulse(dut.tokenizer.new_token) - yield - - # ... we should see the same data transmitted again, with the same PID. - yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) - yield self.assertEqual((yield dut.data_pid), 0) - - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - - # If we do ACK... - yield from self.pulse(dut.handshakes_in.ack) - - # ... we should see our DATA PID flip, and we should be ready to accept data again... - yield self.assertEqual((yield dut.data_pid), 1) - yield self.assertEqual((yield transfer_stream.ready), 1) - - # ... and we should get our second packet. - yield from self.pulse(dut.tokenizer.ready_for_response, step_after=True) - for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: - self.assertEqual((yield packet_stream.payload), value) - yield - - - @usb_domain_test_case - def test_nak_when_not_ready(self): - dut = self.dut - - # We shouldn't initially be NAK'ing anything... - self.assertEqual((yield dut.handshakes_out.nak), 0) - - # ... but if we get an IN token we're not ready for... - yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) - - # ... we should see one cycle of NAK. - self.assertEqual((yield dut.handshakes_out.nak), 1) - yield - self.assertEqual((yield dut.handshakes_out.nak), 0) - - - @usb_domain_test_case - def test_zlp_generation(self): - dut = self.dut - - packet_stream = dut.packet_stream - transfer_stream = dut.transfer_stream - - # Simulate a case where we're generating ZLPs. - yield dut.generate_zlps.eq(1) - - - # If we're sent a full packet _without the transfer stream ending_... - yield transfer_stream.valid.eq(1) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - yield transfer_stream.payload.eq(value) - yield - yield transfer_stream.valid.eq(0) - - - # ... we should receive that data packet without a ZLP. - yield from self.pulse(dut.tokenizer.ready_for_response) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - self.assertEqual((yield dut.data_pid), 0) - yield from self.pulse(dut.handshakes_in.ack) - - - # If we send a full packet... - yield transfer_stream.valid.eq(1) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]: - yield transfer_stream.payload.eq(value) - yield - - # ... that _ends_ our transfer... - yield transfer_stream.payload.eq(0x88) - yield transfer_stream.last.eq(1) - yield - - yield transfer_stream.last.eq(0) - yield transfer_stream.valid.eq(0) - - # ... we should emit the relevant data packet... - yield from self.pulse(dut.tokenizer.ready_for_response) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - self.assertEqual((yield dut.data_pid), 1) - yield from self.pulse(dut.handshakes_in.ack) - - # ... followed by a ZLP. - yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) - self.assertEqual((yield packet_stream.last), 1) - self.assertEqual((yield dut.data_pid), 0) - yield from self.pulse(dut.handshakes_in.ack) - - - # Finally, if we're sent a short packet that ends our stream... - yield transfer_stream.valid.eq(1) - for value in [0xAA, 0xBB, 0xCC]: - yield transfer_stream.payload.eq(value) - yield - yield transfer_stream.payload.eq(0xDD) - yield transfer_stream.last.eq(1) - - yield - yield transfer_stream.last.eq(0) - yield transfer_stream.valid.eq(0) - - # ... we should emit the relevant short packet... - yield from self.pulse(dut.tokenizer.ready_for_response) - for value in [0xAA, 0xBB, 0xCC, 0xDD]: - self.assertEqual((yield packet_stream.payload), value) - yield - yield from self.pulse(dut.handshakes_in.ack) - self.assertEqual((yield dut.data_pid), 1) - - - # ... and we shouldn't emit a ZLP; meaning we should be ready to receive new data. - self.assertEqual((yield transfer_stream.ready), 1) - - - @usb_domain_test_case - def test_discard(self): - dut = self.dut - - packet_stream = dut.packet_stream - transfer_stream = dut.transfer_stream - - # Before we do anything, we shouldn't have anything our output stream. - self.assertEqual((yield packet_stream.valid), 0) - - # Our transfer stream should accept data until we fill up its buffers. - self.assertEqual((yield transfer_stream.ready), 1) - - # We queue up two full packets. - yield transfer_stream.valid.eq(1) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - yield transfer_stream.payload.eq(value) - yield - - for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: - yield transfer_stream.payload.eq(value) - yield - yield transfer_stream.valid.eq(0) - - # Once we do see an IN token... - yield from self.pulse(dut.tokenizer.ready_for_response) - - # ... we should start transmitting... - self.assertEqual((yield packet_stream.valid), 1) - - # ... and should see the full packet be emitted... - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - - # ... with DATA PID 0 ... - self.assertEqual((yield dut.data_pid), 0) - - # ... and then the packet should end. - self.assertEqual((yield packet_stream.valid), 0) - - # If we ACK the first packet... - yield from self.pulse(dut.handshakes_in.ack) - - # ... we should be ready to accept data again. - self.assertEqual((yield transfer_stream.ready), 1) - yield from self.advance_cycles(5) - - # If we then discard the second packet... - yield from self.pulse(dut.discard, step_after=False) - - # ... we shouldn't see a transmit request upon an in token. - yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) - yield from self.advance_cycles(5) - self.assertEqual((yield packet_stream.valid), 0) - - # If we send another full packet... - yield transfer_stream.valid.eq(1) - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - yield transfer_stream.payload.eq(value) - yield - yield transfer_stream.valid.eq(0) - - # ... and see an IN token... - yield from self.pulse(dut.tokenizer.ready_for_response) - - # ... we should start transmitting... - self.assertEqual((yield packet_stream.valid), 1) - - # ... add should see the full packet be emitted... - for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: - self.assertEqual((yield packet_stream.payload), value) - yield - - # ... with the correct DATA PID. - self.assertEqual((yield dut.data_pid), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/application/request.py b/luna/gateware/usb/usb3/application/request.py index cbd34ba82..90cb014a9 100644 --- a/luna/gateware/usb/usb3/application/request.py +++ b/luna/gateware/usb/usb3/application/request.py @@ -5,10 +5,7 @@ # SPDX-License-Identifier: BSD-3-Clause """ Control-request interfacing and gateware for USB3. """ -import unittest - from amaranth import * -from usb_protocol.types import USBRequestType, USBRequestRecipient from ...request import SetupPacket from ...stream import SuperSpeedStreamInterface @@ -16,7 +13,6 @@ from ..protocol.data import DataHeaderPacket from ....utils import falling_edge_detected -from ....test import LunaSSGatewareTestCase, ss_domain_test_case class SuperSpeedRequestHandlerInterface: """ Interface representing a connection between a control endpoint and a request handler. @@ -105,8 +101,6 @@ def __init__(self): self.new_config = Signal(8) - - class SuperSpeedSetupDecoder(Elaboratable): """ Gateware that decodes any received Setup packets. @@ -211,47 +205,6 @@ def elaborate(self, platform): return m - -class SuperSpeedSetupDecoderTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = SuperSpeedSetupDecoder - - @ss_domain_test_case - def test_setup_parse(self): - dut = self.dut - sink = dut.sink - setup = dut.packet - - # Mark our data as always valid SETUP data. - yield dut.header_in.setup.eq(1) - yield sink.valid.eq(0b1111) - - # Provide our first word... - yield sink.first.eq(1) - yield sink.last .eq(0) - yield sink.data .eq(0x2211AAC1) - yield - - # ... then our second ... - yield sink.first.eq(0) - yield sink.last .eq(1) - yield sink.data .eq(0x00043344) - yield - - # ... then mark our packet as good. - yield from self.pulse(dut.rx_good) - yield - - # Finally, check that our fields have been parsed properly. - self.assertEqual((yield setup.is_in_request), 1) - self.assertEqual((yield setup.type), USBRequestType.VENDOR) - self.assertEqual((yield setup.recipient), USBRequestRecipient.INTERFACE) - self.assertEqual((yield setup.request), 0xAA) - self.assertEqual((yield setup.value), 0x2211) - self.assertEqual((yield setup.index), 0x3344) - self.assertEqual((yield setup.length), 4) - - - class SuperSpeedRequestHandlerMultiplexer(Elaboratable): """ Multiplexes multiple RequestHandlers down to a single interface. @@ -450,7 +403,3 @@ def __init__(self): # I/O port # self.interface = SuperSpeedRequestHandlerInterface() - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/link/crc.py b/luna/gateware/usb/usb3/link/crc.py index c603f8324..4cb27fca1 100644 --- a/luna/gateware/usb/usb3/link/crc.py +++ b/luna/gateware/usb/usb3/link/crc.py @@ -5,15 +5,11 @@ # SPDX-License-Identifier: BSD-3-Clause """ CRC computation gateware for USB3. """ - -import unittest import operator import functools from amaranth import * -from ....test import LunaSSGatewareTestCase, ss_domain_test_case - def compute_usb_crc5(protected_bits): """ Generates a 5-bit signal equivalent to the CRC5 check of a given 11-bits. @@ -434,53 +430,3 @@ def elaborate(self, platform): ] return m - - -class DataPacketPayloadCRCTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = DataPacketPayloadCRC - - @ss_domain_test_case - def test_aligned_crc(self): - dut = self.dut - - #yield dut.advance_word.eq(1) - - for i in (0x02000112, 0x40000000): - yield dut.data_input.eq(i) - yield from self.pulse(dut.advance_word, step_after=False) - - self.assertEqual((yield dut.crc), 0x34984B13) - - - @ss_domain_test_case - def test_unaligned_crc(self): - dut = self.dut - - - # Aligned section of a real USB data capture, from a USB flash drive. - aligned_section =[ - 0x03000112, - 0x09000000, - 0x520013FE, - 0x02010100, - ] - - # Present the aligned section... - for i in aligned_section: - yield dut.data_input.eq(i) - yield from self.pulse(dut.advance_word, step_after=False) - - # ... and then our unaligned data. - yield dut.data_input.eq(0x0000_0103) - yield - - # Our next-CRC should indicate the correct value... - self.assertEqual((yield dut.next_crc_2B), 0x540aa487) - - # ...and after advancing, we should see the same value on our CRC output. - yield from self.pulse(dut.advance_2B) - self.assertEqual((yield dut.crc), 0x540aa487) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/link/data.py b/luna/gateware/usb/usb3/link/data.py index 13eab5c2a..0b1d2f72f 100644 --- a/luna/gateware/usb/usb3/link/data.py +++ b/luna/gateware/usb/usb3/link/data.py @@ -5,8 +5,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Data Packet Payload (DPP) management gateware. """ -import unittest - from amaranth import * from usb_protocol.types import USBDirection @@ -17,7 +15,6 @@ from ..physical.coding import SHP, SDP, EPF, stream_matches_symbols from ...stream import USBRawSuperSpeedStream, SuperSpeedStreamInterface -from ....test.utils import LunaSSGatewareTestCase, ss_domain_test_case class DataHeaderPacket(HeaderPacket): @@ -315,95 +312,6 @@ def elaborate(self, platform): return m - -class DataPacketReceiverTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = DataPacketReceiver - - def initialize_signals(self): - yield self.dut.sink.valid.eq(1) - - def provide_data(self, *tuples): - """ Provides the receiver with a sequence of (data, ctrl) values. """ - - # Provide each word of our data to our receiver... - for data, ctrl in tuples: - yield self.dut.sink.data.eq(data) - yield self.dut.sink.ctrl.eq(ctrl) - yield - - - @ss_domain_test_case - def test_unaligned_1B_packet_receive(self): - - # Provide a packet pair to the device. - # (This data is from an actual recorded data packet.) - yield from self.provide_data( - # Header packet. - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x32000008, 0b0000), - (0x00010000, 0b0000), - (0x08000000, 0b0000), - (0xE801A822, 0b0000), - - # Payload packet. - (0xF75C5C5C, 0b1111), - (0x000000FF, 0b0000), - (0xFDFDFDFF, 0b1110), - ) - - self.assertEqual((yield self.dut.packet_good), 1) - - - @ss_domain_test_case - def test_unaligned_2B_packet_receive(self): - - # Provide a packet pair to the device. - # (This data is from an actual recorded data packet.) - yield from self.provide_data( - # Header packet. - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x34000008, 0b0000), - (0x00020000, 0b0000), - (0x08000000, 0b0000), - (0xD005A242, 0b0000), - - # Payload packet. - (0xF75C5C5C, 0b1111), - (0x2C98BBAA, 0b0000), - (0xFDFD4982, 0b1110), - ) - - self.assertEqual((yield self.dut.packet_good), 1) - - - - @ss_domain_test_case - def test_aligned_packet_receive(self): - - # Provide a packet pair to the device. - # (This data is from an actual recorded data packet.) - yield from self.provide_data( - # Header packet. - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x00000008, 0b0000), - (0x00088000, 0b0000), - (0x08000000, 0b0000), - (0xA8023E0F, 0b0000), - - # Payload packet. - (0xF75C5C5C, 0b1111), - (0x001E0500, 0b0000), - (0x00000000, 0b0000), - (0x0EC69325, 0b0000), - ) - - self.assertEqual((yield self.dut.packet_good), 1) - - - class DataPacketTransmitter(Elaboratable): """ Gateware that generates a Data Packet Header, and orchestrates sending it and a payload. @@ -560,7 +468,3 @@ def elaborate(self, platform): return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/link/receiver.py b/luna/gateware/usb/usb3/link/receiver.py index 912d0e0dc..2f789ae64 100644 --- a/luna/gateware/usb/usb3/link/receiver.py +++ b/luna/gateware/usb/usb3/link/receiver.py @@ -5,8 +5,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Header Packet Rx-handling gateware. """ -import unittest - from amaranth import * from usb_protocol.types.superspeed import LinkCommand @@ -17,9 +15,6 @@ from ..physical.coding import SHP, EPF, stream_matches_symbols from ...stream import USBRawSuperSpeedStream -from ....test.utils import LunaSSGatewareTestCase, ss_domain_test_case - - class RawHeaderPacketReceiver(Elaboratable): """ Class that monitors the USB bus for Header Packet, and receives them. @@ -165,112 +160,6 @@ def elaborate(self, platform): return m -class RawHeaderPacketReceiverTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = RawHeaderPacketReceiver - - def initialize_signals(self): - yield self.dut.sink.valid.eq(1) - - def provide_data(self, *tuples): - """ Provides the receiver with a sequence of (data, ctrl) values. """ - - # Provide each word of our data to our receiver... - for data, ctrl in tuples: - yield self.dut.sink.data.eq(data) - yield self.dut.sink.ctrl.eq(ctrl) - yield - - - @ss_domain_test_case - def test_good_packet_receive(self): - dut = self.dut - - # Data input for an actual Link Management packet (seq #0). - yield from self.provide_data( - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x00000280, 0b0000), - (0x00010004, 0b0000), - (0x00000000, 0b0000), - (0x10001845, 0b0000), - ) - - # ... after a cycle to process, we should see an indication that the packet is good. - yield from self.advance_cycles(2) - self.assertEqual((yield dut.new_packet), 1) - self.assertEqual((yield dut.bad_packet), 0) - self.assertEqual((yield dut.bad_sequence), 0) - - - @ss_domain_test_case - def test_bad_sequence_receive(self): - dut = self.dut - - # Expect a sequence number other than the one we'll be providing. - yield dut.expected_sequence.eq(3) - - # Data input for an actual Link Management packet (seq #0). - yield from self.provide_data( - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x00000280, 0b0000), - (0x00010004, 0b0000), - (0x00000000, 0b0000), - (0x10001845, 0b0000), - ) - - # ... after a cycle to process, we should see an indication that the packet is good. - yield from self.advance_cycles(1) - self.assertEqual((yield dut.new_packet), 0) - self.assertEqual((yield dut.bad_packet), 0) - self.assertEqual((yield dut.bad_sequence), 1) - - - - @ss_domain_test_case - def test_bad_packet_receive(self): - dut = self.dut - - # Data input for an actual Link Management packet (seq #0), - # but with the last word corrupted to invalidate our CRC16. - yield from self.provide_data( - # data ctrl - (0xF7FBFBFB, 0b1111), - (0x00000280, 0b0000), - (0x00010004, 0b0000), - (0xFFFFFFFF, 0b0000), - (0x10001845, 0b0000), - ) - - # ... after a cycle to process, we should see an indication that the packet is bad. - yield from self.advance_cycles(1) - self.assertEqual((yield dut.new_packet), 0) - self.assertEqual((yield dut.bad_packet), 1) - self.assertEqual((yield dut.bad_sequence), 0) - - - @ss_domain_test_case - def test_bad_crc_and_sequence_receive(self): - dut = self.dut - - # Completely invalid link packet, guaranteed to have a bad sequence number & CRC. - yield from self.provide_data( - # data ctrl - (0xF7FBFBFB, 0b1111), - (0xFFFFFFFF, 0b0000), - (0xFFFFFFFF, 0b0000), - (0xFFFFFFFF, 0b0000), - (0xFFFFFFFF, 0b0000), - ) - - # Once we've processed this, we should see that there's a bad packet; but that it's - # corrupted enough that our sequence no longer matters. - yield from self.advance_cycles(1) - self.assertEqual((yield dut.new_packet), 0) - self.assertEqual((yield dut.bad_packet), 1) - self.assertEqual((yield dut.bad_sequence), 0) - - class HeaderPacketReceiver(Elaboratable): """ Receiver-side Header Packet logic. @@ -742,7 +631,3 @@ def elaborate(self, platform): m.next = "DISPATCH_COMMAND" return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/physical/coding.py b/luna/gateware/usb/usb3/physical/coding.py index 8dd545276..7d8f770f3 100644 --- a/luna/gateware/usb/usb3/physical/coding.py +++ b/luna/gateware/usb/usb3/physical/coding.py @@ -39,13 +39,13 @@ def __init__(self, name, value, description="", is_data=False): def value_const(self, *, repeat=1): """ Returns this symbol's data value as an Amaranth const. """ value = Const(self.value, 8) - return Repl(value, repeat) + return value.replicate(repeat) def ctrl_const(self, *, repeat=1): """ Returns this symbol's ctrl value as an Amaranth const. """ ctrl = Const(self.ctrl, 1) - return Repl(ctrl, repeat) + return ctrl.replicate(repeat) SKP = NamedSymbol("SKP", K(28, 1), "Skip") # 3c diff --git a/luna/gateware/usb/usb3/physical/ctc.py b/luna/gateware/usb/usb3/physical/ctc.py index d0cd6e435..ea07efdeb 100644 --- a/luna/gateware/usb/usb3/physical/ctc.py +++ b/luna/gateware/usb/usb3/physical/ctc.py @@ -17,15 +17,11 @@ It's up to us to insert and remove additional ordered sets. """ -import unittest - from amaranth import * from .coding import SKP, stream_word_matches_symbol from ...stream import USBRawSuperSpeedStream -from ....test.utils import LunaSSGatewareTestCase, ss_domain_test_case - class CTCSkipRemover(Elaboratable): """ Clock Tolerance Compensation (CTC) receive buffer gateware. @@ -219,143 +215,6 @@ def elaborate(self, platform): return m - -class CTCSkipRemoverTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = CTCSkipRemover - - def initialize_signals(self): - # Set up our streams to always ferry data in and out, where possible. - yield self.dut.sink.valid.eq(1) - yield self.dut.source.ready.eq(1) - - - def provide_input(self, data, ctrl): - yield self.dut.sink.data.eq(data) - yield self.dut.sink.ctrl.eq(ctrl) - yield - - - @ss_domain_test_case - def test_dual_skip_removal(self): - source = self.dut.source - - # When we add data into the buffer... - yield from self.provide_input(0xAABBCCDD, 0b0000) - - # ... we should see our line go valid only after four bytes are collected. - self.assertEqual((yield source.valid), 0) - yield from self.provide_input(0x71BA3C3C, 0b0011) - - # Once it does go high, it should be accompanied by valid input data. - self.assertEqual((yield source.valid), 1) - self.assertEqual((yield source.data), 0xAABBCCDD) - self.assertEqual((yield source.ctrl), 0) - yield from self.provide_input(0x11223344, 0b1100) - - # If data with SKPs were provided, our output should be invalid, until we - # receive enough bytes to have four non-skip bytes. - self.assertEqual((yield source.valid), 0) - - # Once we do, we should see a copy of our data without the SKPs included. - yield - self.assertEqual((yield source.data), 0x334471BA) - self.assertEqual((yield source.ctrl), 0) - yield - self.assertEqual((yield source.data), 0x33441122) - self.assertEqual((yield source.ctrl), 0b11) - - - @ss_domain_test_case - def test_shifted_dual_skip_removal(self): - source = self.dut.source - - # When we add data into the buffer... - yield from self.provide_input(0xAABBCCDD, 0b0000) - - # ... we should see our line go valid only after four bytes are collected. - self.assertEqual((yield source.valid), 0) - yield from self.provide_input(0x713C3CBA, 0b0110) - - # Once it does go high, it should be accompanied by valid input data. - self.assertEqual((yield source.valid), 1) - self.assertEqual((yield source.data), 0xAABBCCDD) - self.assertEqual((yield source.ctrl), 0) - yield from self.provide_input(0x113C3C44, 0b0110) - - # If data with SKPs were provided, our output should be invalid, until we - # receive enough bytes to have four non-skip bytes. - self.assertEqual((yield source.valid), 0) - - # Once we do, we should see a copy of our data without the SKPs included. - yield from self.provide_input(0x55667788, 0b0000) - self.assertEqual((yield source.data), 0x114471BA) - self.assertEqual((yield source.ctrl), 0) - yield - self.assertEqual((yield source.data), 0x55667788) - self.assertEqual((yield source.ctrl), 0) - - - @ss_domain_test_case - def test_single_skip_removal(self): - source = self.dut.source - - # When we add data into the buffer... - yield from self.provide_input(0xAABBCCDD, 0b0000) - - # ... we should see our line go valid only after four bytes are collected. - self.assertEqual((yield source.valid), 0) - yield from self.provide_input(0x3C556677, 0b1000) - - # Once it does go high, it should be accompanied by valid input data. - self.assertEqual((yield source.valid), 1) - self.assertEqual((yield source.data), 0xAABBCCDD) - self.assertEqual((yield source.ctrl), 0) - yield from self.provide_input(0x11223344, 0b1100) - - # If data with SKPs were provided, our output should be invalid, until we - # receive enough bytes to have four non-skip bytes. - self.assertEqual((yield source.valid), 0) - - # Once we do, we should see a copy of our data without the SKPs included. - yield - self.assertEqual((yield source.data), 0x44556677) - self.assertEqual((yield source.ctrl), 0) - yield - self.assertEqual((yield source.data), 0x44112233) - self.assertEqual((yield source.ctrl), 0b110) - - - @ss_domain_test_case - def test_cycle_spread_skip_removal(self): - source = self.dut.source - - # When we add data into the buffer... - yield from self.provide_input(0xAABBCCDD, 0b0000) - - # ... we should see our line go valid only after four bytes are collected. - self.assertEqual((yield source.valid), 0) - yield from self.provide_input(0x3C556677, 0b1000) - - # Once it does go high, it should be accompanied by valid input data. - self.assertEqual((yield source.valid), 1) - self.assertEqual((yield source.data), 0xAABBCCDD) - self.assertEqual((yield source.ctrl), 0) - yield from self.provide_input(0x1122333C, 0b0001) - - # If data with SKPs were provided, our output should be invalid, until we - # receive enough bytes to have four non-skip bytes. - self.assertEqual((yield source.valid), 0) - - # Once we do, we should see a copy of our data without the SKPs included. - yield from self.provide_input(0x44556677, 0b0000) - self.assertEqual((yield source.data), 0x33556677) - self.assertEqual((yield source.ctrl), 0) - yield - self.assertEqual((yield source.data), 0x66771122) - self.assertEqual((yield source.ctrl), 0b0) - - - class CTCSkipInserter(Elaboratable): """ Clock Tolerance Compensation (CTC) Skip insertion gateware. @@ -454,8 +313,8 @@ def elaborate(self, platform): m.d.comb += self.sending_skip.eq(1) m.d.ss += [ source.valid .eq(1), - source.data .eq(Repl(SKP.value_const(), len(source.ctrl))), - source.ctrl .eq(Repl(SKP.ctrl_const(), len(source.ctrl))), + source.data .eq(SKP.value_const().replicate(len(source.ctrl))), + source.ctrl .eq(SKP.ctrl_const().replicate(len(source.ctrl))), ] with m.Else(): @@ -464,7 +323,3 @@ def elaborate(self, platform): ] return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/physical/lfps.py b/luna/gateware/usb/usb3/physical/lfps.py index 4a46bd3ef..b8cff422c 100644 --- a/luna/gateware/usb/usb3/physical/lfps.py +++ b/luna/gateware/usb/usb3/physical/lfps.py @@ -26,12 +26,10 @@ and to detect RX electrical idle. """ -import unittest from math import ceil from amaranth import * -from ....test.utils import LunaSSGatewareTestCase, ss_domain_test_case from ....utils import synchronize, rising_edge_detected @@ -277,47 +275,6 @@ def elaborate(self, platform): return m -class LFPSGeneratorTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = LFPSGenerator - FRAGMENT_ARGUMENTS = dict( - lfps_pattern = _PollingLFPS, - sys_clk_freq = 125e6 - ) - - @ss_domain_test_case - def test_polling_lfps_sequence(self): - dut = self.dut - - burst_length = ceil(self.SS_CLOCK_FREQUENCY * _PollingLFPSBurst.t_typ) - burst_repeat = ceil(self.SS_CLOCK_FREQUENCY * _PollingLFPSRepeat.t_typ) - - # Trigger a burst... - yield dut.generate.eq(1) - yield - yield - yield dut.generate.eq(0) - - # Wait for a whole burst-repeat cycle... - burst_ticks = 0 - total_ticks = 0 - while (yield dut.drive_electrical_idle): - - # ... and measure how long our burst lasts... - if (yield dut.send_signaling): - burst_ticks += 1 - - # ... as well as the length of our whole interval. - total_ticks += 1 - yield - - # Our observed burst length should be within 10% of our specification... - self.assertLess(abs(burst_ticks)/burst_length - 1.0, 10e-2) - - # ... as should our observed total length between bursts. - self.assertLess(abs(total_ticks)/burst_repeat - 1.0, 10e-2) - - - class LFPSTransceiver(Elaboratable): """ Low-Frequency Periodic Signaling (LFPS) Transciever @@ -398,7 +355,3 @@ def elaborate(self, platform): m.d.ss += self.cycles_sent.eq(0) return m - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/physical/scrambling.py b/luna/gateware/usb/usb3/physical/scrambling.py index 93d3edbe2..32e7af111 100644 --- a/luna/gateware/usb/usb3/physical/scrambling.py +++ b/luna/gateware/usb/usb3/physical/scrambling.py @@ -8,7 +8,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Scrambling and descrambling for USB3. """ -import unittest import operator import functools @@ -17,8 +16,6 @@ from .coding import COM, stream_word_matches_symbol from ...stream import USBRawSuperSpeedStream -from ....test.utils import LunaSSGatewareTestCase, ss_domain_test_case - # # Scrambling modules. @@ -136,30 +133,6 @@ def xor_bits(*indices): return m -class ScramblerLFSRTest(LunaSSGatewareTestCase): - FRAGMENT_UNDER_TEST = ScramblerLFSR - - @ss_domain_test_case - def test_lfsr_stream(self): - # From the table of 8-bit encoded values, [USB3.2, Appendix B.1]. - # We can continue this as long as we want to get more thorough testing, - # but for now, this is probably enough. - scrambled_sequence = [ - 0x14c017ff, 0x8202e7b2, 0xa6286e72, 0x8dbf6dbe, # Row 1 (0x00) - 0xe6a740be, 0xb2e2d32c, 0x2a770207, 0xe0be34cd, # Row 2 (0x10) - 0xb1245da7, 0x22bda19b, 0xd31d45d4, 0xee76ead7 # Row 3 (0x20) - ] - - yield self.dut.advance.eq(1) - yield - - # Check that our LFSR produces each of our values in order. - for index, value in enumerate(scrambled_sequence): - self.assertEqual((yield self.dut.value), value, f"incorrect value at cycle {index}") - yield - - - class Scrambler(Elaboratable): """ USB3-compliant data scrambler. @@ -269,7 +242,3 @@ class Descrambler(Scrambler): def __init__(self, initial_value=0xffff): self._initial_value = initial_value super().__init__(initial_value=initial_value) - - -if __name__ == "__main__": - unittest.main() diff --git a/luna/gateware/usb/usb3/request/standard.py b/luna/gateware/usb/usb3/request/standard.py index 8a7bf102e..202b1d2f8 100644 --- a/luna/gateware/usb/usb3/request/standard.py +++ b/luna/gateware/usb/usb3/request/standard.py @@ -5,8 +5,6 @@ # SPDX-License-Identifier: BSD-3-Clause """ Standard, full-gateware control request handlers. """ -import unittest - from amaranth import * from usb_protocol.types import USBStandardRequests, USBRequestType @@ -250,8 +248,3 @@ def elaborate(self, platform): m.next = 'IDLE' return m - - -if __name__ == "__main__": - unittest.main(warnings="ignore") - diff --git a/luna/gateware/utils/cdc.py b/luna/gateware/utils/cdc.py index 02620a992..cc11dbac1 100644 --- a/luna/gateware/utils/cdc.py +++ b/luna/gateware/utils/cdc.py @@ -7,16 +7,11 @@ """ Helpers for clock domain crossings. """ -import unittest -import warnings - -from unittest import TestCase -from amaranth import Record, Module, Signal +from amaranth import Record, Signal from amaranth.lib.cdc import FFSynchronizer from amaranth.lib.io import Pin -from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT +from amaranth.hdl.rec import DIR_FANOUT -from ..test import LunaGatewareTestCase, sync_test_case def synchronize(m, signal, *, output=None, o_domain='sync', stages=2): """ Convenience function. Synchronizes a signal, or equivalent collection. @@ -76,35 +71,6 @@ def create_synchronizer(signal, output): return output -class SynchronizedTest(TestCase): - - def test_signal(self): - m = Module() - synchronize(m, Signal()) - - def test_directional_record(self): - m = Module() - - record = Record([ - ('sig_in', 1, DIR_FANIN), - ('sig_out', 1, DIR_FANOUT) - ]) - synchronize(m, record) - - def test_nested_record(self): - m = Module() - - record = Record([ - ('sig_in', 1, DIR_FANIN), - ('sig_out', 1, DIR_FANOUT), - ('nested', [ - ('subsig_in', 1, DIR_FANIN), - ('subsig_out', 1, DIR_FANOUT), - ]) - ]) - synchronize(m, record) - - def stretch_strobe_signal(m, strobe, *, to_cycles, output=None, domain=None, allow_delay=False): """ Stretches a given strobe to the given number of cycles. @@ -145,55 +111,3 @@ def stretch_strobe_signal(m, strobe, *, to_cycles, output=None, domain=None, all m.d.comb += output.eq(strobe | (delayed_strobe != 0)) return output - - -class StrobeStretcherTest(LunaGatewareTestCase): - """ Test case for our strobe stretcher function. """ - - - def instantiate_dut(self): - m = Module() - - # Create a module that only has our stretched strobe signal. - m.strobe_in = Signal() - m.stretched_strobe = stretch_strobe_signal(m, m.strobe_in, to_cycles=2) - - return m - - - def initialize_signals(self): - yield self.dut.strobe_in.eq(0) - - - @sync_test_case - def test_stretch(self): - - # Ensure our stretched strobe stays 0 until it sees an input. - yield - self.assertEqual((yield self.dut.stretched_strobe), 0) - yield - self.assertEqual((yield self.dut.stretched_strobe), 0) - - # Apply our strobe, and validate that we immediately see a '1'... - yield self.dut.strobe_in.eq(1) - yield - self.assertEqual((yield self.dut.stretched_strobe), 1) - - # ... ensure that 1 lasts for a second cycle ... - yield self.dut.strobe_in.eq(0) - yield - self.assertEqual((yield self.dut.stretched_strobe), 1) - - # ... and then returns to 0. - yield - self.assertEqual((yield self.dut.stretched_strobe), 0) - - yield - self.assertEqual((yield self.dut.stretched_strobe), 0) - - - -if __name__ == "__main__": - warnings.filterwarnings("error") - unittest.main() - diff --git a/luna/gateware/utils/io.py b/luna/gateware/utils/io.py index 37395f258..a92061e42 100644 --- a/luna/gateware/utils/io.py +++ b/luna/gateware/utils/io.py @@ -7,9 +7,6 @@ """ Helpers for I/O interfacing. """ -import unittest -from unittest import TestCase - from amaranth import Record, Instance, Module, Signal, Cat from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT diff --git a/pyproject.toml b/pyproject.toml index 2791899eb..23c1b0512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,6 @@ dev = [ [tool.setuptools.packages.find] where = ["."] include = ["*"] + +[tool.pdm.scripts] +test.cmd = "python -m unittest discover -t . -s tests -v" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bf7ac46f6..000000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -tox -pyusb -git+https://github.com/amaranth-lang/amaranth --e git+https://github.com/amaranth-lang/amaranth-soc.git#egg=amaranth-soc --e git+https://github.com/amaranth-lang/amaranth-boards.git#egg=amaranth_boards --e git+https://github.com/lambdaconcept/minerva.git#egg=minerva --e git+https://github.com/usb-tools/python-usb-protocol.git#egg=usb_protocol --e git+https://github.com/greatscottgadgets/apollo.git#egg=apollo-fpga -pyvcd -pyserial~=3.4 -libusb1 diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index 00ec7aa86..000000000 --- a/requirements_test.txt +++ /dev/null @@ -1,7 +0,0 @@ -tox -pyusb -amaranth~=0.4.0 --e git+https://github.com/amaranth-lang/amaranth-boards.git#egg=amaranth_boards --e git+https://github.com/usb-tools/python-usb-protocol.git#egg=usb_protocol -pyvcd -pyserial~=3.4 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_car.py b/tests/test_car.py new file mode 100644 index 000000000..400260810 --- /dev/null +++ b/tests/test_car.py @@ -0,0 +1,39 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, sync_test_case + +from luna.gateware.architecture.car import PHYResetController + +class PHYResetControllerTest(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = PHYResetController + + def initialize_signals(self): + yield self.dut.trigger.eq(0) + + @sync_test_case + def test_power_on_reset(self): + + # + # After power-on, the PHY should remain in reset for a while. + # + yield + self.assertEqual((yield self.dut.phy_reset), 1) + + yield from self.advance_cycles(30) + self.assertEqual((yield self.dut.phy_reset), 1) + + yield from self.advance_cycles(60) + self.assertEqual((yield self.dut.phy_reset), 1) + + # + # Then, after the relevant reset time, it should resume being unasserted. + # + yield from self.advance_cycles(31) + self.assertEqual((yield self.dut.phy_reset), 0) + self.assertEqual((yield self.dut.phy_stop), 1) + + yield from self.advance_cycles(120) + self.assertEqual((yield self.dut.phy_stop), 0) diff --git a/tests/test_cdc.py b/tests/test_cdc.py new file mode 100644 index 000000000..2852ac255 --- /dev/null +++ b/tests/test_cdc.py @@ -0,0 +1,85 @@ +# amaranth: UnusedElaboratable=no +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, sync_test_case +from unittest import TestCase + +from amaranth import Module, Record, Signal +from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT +from luna.gateware.utils.cdc import synchronize, stretch_strobe_signal + +class SynchronizedTest(TestCase): + + def test_signal(self): + m = Module() + synchronize(m, Signal()) + + def test_directional_record(self): + m = Module() + + record = Record([ + ('sig_in', 1, DIR_FANIN), + ('sig_out', 1, DIR_FANOUT) + ]) + synchronize(m, record) + + def test_nested_record(self): + m = Module() + + record = Record([ + ('sig_in', 1, DIR_FANIN), + ('sig_out', 1, DIR_FANOUT), + ('nested', [ + ('subsig_in', 1, DIR_FANIN), + ('subsig_out', 1, DIR_FANOUT), + ]) + ]) + synchronize(m, record) + + +class StrobeStretcherTest(LunaGatewareTestCase): + """ Test case for our strobe stretcher function. """ + + + def instantiate_dut(self): + m = Module() + + # Create a module that only has our stretched strobe signal. + m.strobe_in = Signal() + m.stretched_strobe = stretch_strobe_signal(m, m.strobe_in, to_cycles=2) + + return m + + + def initialize_signals(self): + yield self.dut.strobe_in.eq(0) + + + @sync_test_case + def test_stretch(self): + + # Ensure our stretched strobe stays 0 until it sees an input. + yield + self.assertEqual((yield self.dut.stretched_strobe), 0) + yield + self.assertEqual((yield self.dut.stretched_strobe), 0) + + # Apply our strobe, and validate that we immediately see a '1'... + yield self.dut.strobe_in.eq(1) + yield + self.assertEqual((yield self.dut.stretched_strobe), 1) + + # ... ensure that 1 lasts for a second cycle ... + yield self.dut.strobe_in.eq(0) + yield + self.assertEqual((yield self.dut.stretched_strobe), 1) + + # ... and then returns to 0. + yield + self.assertEqual((yield self.dut.stretched_strobe), 0) + + yield + self.assertEqual((yield self.dut.stretched_strobe), 0) diff --git a/tests/test_i2c.py b/tests/test_i2c.py new file mode 100644 index 000000000..d8b470b9f --- /dev/null +++ b/tests/test_i2c.py @@ -0,0 +1,164 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.interface.spi import SPIGatewareTestCase +from luna.gateware.test import LunaGatewareTestCase, sync_test_case + +from amaranth import Signal +from luna.gateware.interface.i2c import I2CBus, I2CInitiator + +class I2CInitiatorTestbench(I2CInitiator): + def __init__(self, pads, period_cyc, clk_stretch=True): + super().__init__(pads, period_cyc, clk_stretch) + self.scl_o = Signal(reset=1) # used to override values from testbench + self.sda_o = Signal(reset=1) + + def elaborate(self, platform): + m = super().elaborate(platform) + m.d.comb += [ + self.bus.scl_t.i.eq((self.bus.scl_t.o | ~self.bus.scl_t.oe) & self.scl_o), + self.bus.sda_t.i.eq((self.bus.sda_t.o | ~self.bus.sda_t.oe) & self.sda_o), + ] + return m + + +class TestI2CInitiator(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = I2CInitiatorTestbench + FRAGMENT_ARGUMENTS = { "pads": I2CBus(), "period_cyc": 16 } + + def wait_condition(self, strobe): + yield from self.wait_until(strobe, timeout=3*self.dut.period_cyc) + + def start(self): + yield from self.pulse(self.dut.start) + yield from self.wait_condition(self.dut.bus.start) + + def stop(self): + yield from self.pulse(self.dut.stop) + yield from self.wait_condition(self.dut.bus.stop) + + @sync_test_case + def test_start(self): + yield from self.start() + self.assertEqual((yield self.dut.busy), 0) + + @sync_test_case + def test_repeated_start(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.start() + yield from self.wait_condition(self.dut.bus.start) + self.assertEqual((yield self.dut.busy), 0) + + @sync_test_case + def test_stop(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.stop() + self.assertEqual((yield self.dut.busy), 0) + + def write(self, data, bits, ack): + yield self.dut.data_i.eq(data) + yield from self.pulse(self.dut.write) + for n, bit in enumerate(bits): + yield + yield + yield from self.wait_condition(self.dut.bus.scl_i == 0) + yield from self.wait_condition(self.dut.bus.scl_i == 1) + self.assertEqual((yield self.dut.bus.sda_i), bit) + yield + yield from self.advance_cycles(self.dut.period_cyc // 2) + yield + yield + yield from self.wait_condition(self.dut.bus.scl_i == 0) + yield self.dut.sda_o.eq(not ack) + yield from self.wait_condition(self.dut.bus.scl_i == 1) + yield self.dut.sda_o.eq(1) + self.assertEqual((yield self.dut.busy), 1) + yield from self.advance_cycles(self.dut.period_cyc // 2) + yield + yield + yield + yield + self.assertEqual((yield self.dut.busy), 0) + self.assertEqual((yield self.dut.ack_o), ack) + + @sync_test_case + def test_write_ack(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.write(0xA5, [1, 0, 1, 0, 0, 1, 0, 1], 1) + + @sync_test_case + def test_write_nak(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.write(0x5A, [0, 1, 0, 1, 1, 0, 1, 0], 0) + + @sync_test_case + def test_write_tx(self): + yield from self.start() + yield from self.write(0x55, [0, 1, 0, 1, 0, 1, 0, 1], 1) + yield from self.write(0x33, [0, 0, 1, 1, 0, 0, 1, 1], 0) + yield from self.stop() + yield + yield + self.assertEqual((yield self.dut.bus.sda_i), 1) + self.assertEqual((yield self.dut.bus.scl_i), 1) + + def read(self, data, bits, ack): + yield self.dut.ack_i.eq(ack) + yield from self.pulse(self.dut.read) + for n, bit in enumerate(bits): + yield + yield + yield from self.wait_condition(self.dut.bus.scl_i == 0) + yield self.dut.sda_o.eq(bit) + yield from self.wait_condition(self.dut.bus.scl_i == 1) + yield + yield self.dut.sda_o.eq(1) + yield from self.advance_cycles(self.dut.period_cyc // 2) + yield + yield + yield from self.wait_condition(self.dut.bus.scl_i == 0) + yield from self.wait_condition(self.dut.bus.scl_i == 1) + self.assertEqual((yield self.dut.bus.sda_i), not ack) + self.assertEqual((yield self.dut.busy), 1) + yield from self.advance_cycles(self.dut.period_cyc // 2) + yield + yield + yield + yield + self.assertEqual((yield self.dut.busy), 0) + self.assertEqual((yield self.dut.data_o), data) + + @sync_test_case + def test_read_ack(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.read(0xA5, [1, 0, 1, 0, 0, 1, 0, 1], 1) + + @sync_test_case + def test_read_nak(self): + yield self.dut.bus.sda_o.eq(0) + yield + yield + yield from self.read(0x5A, [0, 1, 0, 1, 1, 0, 1, 0], 0) + + @sync_test_case + def test_read_tx(self): + yield from self.start() + yield from self.read(0x55, [0, 1, 0, 1, 0, 1, 0, 1], 1) + yield from self.read(0x33, [0, 0, 1, 1, 0, 0, 1, 1], 0) + yield from self.stop() + yield + yield + self.assertEqual((yield self.dut.bus.sda_i), 1) + self.assertEqual((yield self.dut.bus.scl_i), 1) diff --git a/tests/test_ila.py b/tests/test_ila.py new file mode 100644 index 000000000..6f2b41e7d --- /dev/null +++ b/tests/test_ila.py @@ -0,0 +1,207 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.interface.spi import SPIGatewareTestCase +from luna.gateware.test import LunaGatewareTestCase, sync_test_case + +from amaranth import Signal, Cat +from luna.gateware.debug.ila import IntegratedLogicAnalyzer, StreamILA, SyncSerialILA + +class IntegratedLogicAnalyzerTest(LunaGatewareTestCase): + + def instantiate_dut(self): + self.input_a = Signal() + self.input_b = Signal(30) + self.input_c = Signal() + + return IntegratedLogicAnalyzer( + signals=[self.input_a, self.input_b, self.input_c], + sample_depth = 32 + ) + + + def initialize_signals(self): + yield self.input_a .eq(0) + yield self.input_b .eq(0) + yield self.input_c .eq(0) + + + def provide_all_signals(self, value): + all_signals = Cat(self.input_a, self.input_b, self.input_c) + yield all_signals.eq(value) + + + def assert_sample_value(self, address, value): + """ Helper that asserts a ILA sample has a given value. """ + + yield self.dut.captured_sample_number.eq(address) + yield + # Delay a clock to allow the block ram to latch the new value + yield + try: + self.assertEqual((yield self.dut.captured_sample), value) + return + except AssertionError: + pass + + # Generate an appropriate exception. + actual_value = (yield self.dut.captured_sample) + message = "assertion failed: at address 0x{:08x}: {:08x} != {:08x} (expected)".format(address, actual_value, value) + raise AssertionError(message) + + + @sync_test_case + def test_sampling(self): + + # Quick helper that generates simple, repetitive samples. + def sample_value(i): + return i | (i << 8) | (i << 16) | (0xFF << 24) + + yield from self.provide_all_signals(0xDEADBEEF) + yield + + # Before we trigger, we shouldn't be capturing any samples, + # and we shouldn't be complete. + self.assertEqual((yield self.dut.sampling), 0) + self.assertEqual((yield self.dut.complete), 0) + + # Advance a bunch of cycles, and ensure we don't start sampling. + yield from self.advance_cycles(10) + self.assertEqual((yield self.dut.sampling), 0) + + # Set a new piece of data for a couple of cycles. + yield from self.provide_all_signals(0x01234567) + yield + yield from self.provide_all_signals(0x89ABCDEF) + yield + + # Finally, trigger the capture. + yield from self.provide_all_signals(sample_value(0)) + yield from self.pulse(self.dut.trigger, step_after=False) + + yield from self.provide_all_signals(sample_value(1)) + yield + + # After we pulse our trigger strobe, we should be sampling. + self.assertEqual((yield self.dut.sampling), 1) + + # Populate the memory with a variety of interesting signals; + # and continue afterwards for a couple of cycles to make sure + # these don't make it into our sample buffer. + for i in range(2, 34): + yield from self.provide_all_signals(sample_value(i)) + yield + + # We now should be done with our sampling. + self.assertEqual((yield self.dut.sampling), 0) + self.assertEqual((yield self.dut.complete), 1) + + # Validate the memory values that were captured. + for i in range(32): + yield from self.assert_sample_value(i, sample_value(i)) + + # All of those reads shouldn't change our completeness. + self.assertEqual((yield self.dut.sampling), 0) + self.assertEqual((yield self.dut.complete), 1) + + +class SyncSerialReadoutILATest(SPIGatewareTestCase): + + def instantiate_dut(self): + self.input_signal = Signal(12) + return SyncSerialILA( + signals=[self.input_signal], + sample_depth=16, + clock_polarity=1, + clock_phase=0 + ) + + def initialize_signals(self): + yield self.input_signal.eq(0xF00) + + @sync_test_case + def test_spi_readout(self): + input_signal = self.input_signal + + # Trigger the test while offering our first sample. + yield + yield from self.pulse(self.dut.trigger, step_after=False) + + # Provide the remainder of our samples. + for i in range(1, 16): + yield input_signal.eq(0xF00 | i) + yield + + # Wait a few cycles to account for delays in + # the sampling pipeline. + yield from self.advance_cycles(5) + + # We've now captured a full set of samples. + # We'll test reading them out. + self.assertEqual((yield self.dut.complete), 1) + + # Start the transaction, and exchange 16 bytes of data. + yield self.dut.spi.cs.eq(1) + yield + + # Read our our result over SPI... + data = yield from self.spi_exchange_data(b"\0" * 32) + + # ... and ensure it matches what was sampled. + i = 0 + while data: + datum = data[0:4] + del data[0:4] + + expected = b"\x00\x00\x0f" + bytes([i]) + self.assertEqual(datum, expected) + i += 1 + +class StreamILATest(LunaGatewareTestCase): + + def instantiate_dut(self): + self.input_signal = Signal(12) + return StreamILA( + signals=[self.input_signal], + sample_depth=16 + ) + + def initialize_signals(self): + yield self.input_signal.eq(0xF00) + + @sync_test_case + def test_stream_readout(self): + input_signal = self.input_signal + stream = self.dut.stream + + # Trigger the ILA with the first sample + yield + yield from self.pulse(self.dut.trigger, step_after=False) + + # Fill up the ILA with the remaining samples + for i in range(1, 16): + yield input_signal.eq(0xF00 | i) + yield + + # Wait a few cycles to allow the ILA to fully finish processing + yield from self.advance_cycles(6) + # Stream should now be presenting valid data + self.assertEqual((yield stream.valid), 1) + + # Now we want to stream out the samples from the ILA + yield stream.ready.eq(1) + yield + self.assertEqual((yield stream.first), 1) + + # Read out data from the stream until it signals completion + data = [] + while not (yield stream.last): + if (yield stream.valid): + data.append((yield stream.payload)) + yield + + # Match read data to what should have been sampled + for i, datum in enumerate(data): + self.assertEqual(datum, 0xF00 | i) diff --git a/tests/test_memory.py b/tests/test_memory.py new file mode 100644 index 000000000..d6c82b710 --- /dev/null +++ b/tests/test_memory.py @@ -0,0 +1,127 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, sync_test_case + +from luna.gateware.memory import TransactionalizedFIFO + +class TransactionalizedFIFOTest(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = TransactionalizedFIFO + FRAGMENT_ARGUMENTS = {'width': 8, 'depth': 16} + + def initialize_signals(self): + yield self.dut.write_en.eq(0) + + @sync_test_case + def test_simple_fill(self): + dut = self.dut + + # Our FIFO should start off empty; and with a full depth of free space. + self.assertEqual((yield dut.empty), 1) + self.assertEqual((yield dut.full), 0) + self.assertEqual((yield dut.space_available), 16) + + # If we add a byte to the queue... + yield dut.write_data.eq(0xAA) + yield from self.pulse(dut.write_en) + + # ... we should have less space available ... + self.assertEqual((yield dut.space_available), 15) + + # ... but we still should be "empty", as we won't have data to read until we commit. + self.assertEqual((yield dut.empty), 1) + + # Once we _commit_ our write, we should suddenly have data to read. + yield from self.pulse(dut.write_commit) + self.assertEqual((yield dut.empty), 0) + + # If we read a byte, we should see the FIFO become empty... + yield from self.pulse(dut.read_en) + self.assertEqual((yield dut.empty), 1) + + # ... but we shouldn't see more space become available until we commit the read. + self.assertEqual((yield dut.space_available), 15) + yield from self.pulse(dut.read_commit) + self.assertEqual((yield dut.space_available), 16) + + # If we write 16 more bytes of data... + yield dut.write_en.eq(1) + for i in range(16): + yield dut.write_data.eq(i) + yield + yield dut.write_en.eq(0) + + # ... our buffer should be full, but also empty. + # This paradox exists as we've filled our buffer with uncomitted data. + yield + self.assertEqual((yield dut.full), 1) + self.assertEqual((yield dut.empty), 1) + + # Once we _commit_ our data, it should suddenly stop being empty. + yield from self.pulse(dut.write_commit) + self.assertEqual((yield dut.empty), 0) + + # Reading a byte _without committing_ shouldn't change anything about empty/full/space-available... + yield from self.pulse(dut.read_en) + self.assertEqual((yield dut.empty), 0) + self.assertEqual((yield dut.full), 1) + self.assertEqual((yield dut.space_available), 0) + + # ... but committing should increase our space available by one, and make our buffer no longer full. + yield from self.pulse(dut.read_commit) + self.assertEqual((yield dut.empty), 0) + self.assertEqual((yield dut.full), 0) + self.assertEqual((yield dut.space_available), 1) + + # Reading/committing another byte should increment our space available. + yield from self.pulse(dut.read_en) + yield from self.pulse(dut.read_commit) + self.assertEqual((yield dut.space_available), 2) + + # Writing data into the buffer should then fill it back up again... + yield dut.write_en.eq(1) + for i in range(2): + yield dut.write_data.eq(i) + yield + yield dut.write_en.eq(0) + + # ... meaning it will again be full, and have no space remaining. + yield + self.assertEqual((yield dut.full), 1) + self.assertEqual((yield dut.space_available), 0) + + # If we _discard_ this data, we should go back to having two bytes available. + yield from self.pulse(dut.write_discard) + self.assertEqual((yield dut.full), 0) + self.assertEqual((yield dut.space_available), 2) + + # If we read the data that's remaining in the fifo... + yield dut.read_en.eq(1) + for i in range(2, 16): + yield + self.assertEqual((yield dut.read_data), i) + yield dut.read_en.eq(0) + + # ... our buffer should again be empty. + yield + self.assertEqual((yield dut.empty), 1) + self.assertEqual((yield dut.space_available), 2) + + # If we _discard_ our current read, we should then see our buffer no longer empty... + yield from self.pulse(dut.read_discard) + self.assertEqual((yield dut.empty), 0) + + # and we should be able to read the same data again. + yield dut.read_en.eq(1) + for i in range(2, 16): + yield + self.assertEqual((yield dut.read_data), i) + yield dut.read_en.eq(0) + + # On committing this, we should see a buffer that is no longer full, and is really empty. + yield from self.pulse(dut.read_commit) + self.assertEqual((yield dut.empty), 1) + self.assertEqual((yield dut.full), 0) + self.assertEqual((yield dut.space_available), 16) diff --git a/tests/test_psram.py b/tests/test_psram.py new file mode 100644 index 000000000..fd4acfeb1 --- /dev/null +++ b/tests/test_psram.py @@ -0,0 +1,180 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test.utils import LunaGatewareTestCase, sync_test_case + +from luna.gateware.interface.psram import HyperBusPHY, HyperRAMInterface + +class TestHyperRAMInterface(LunaGatewareTestCase): + + def instantiate_dut(self): + # Create a record that recreates the layout of our RAM signals. + self.ram_signals = HyperBusPHY() + + # Create our HyperRAM interface... + return HyperRAMInterface(phy=self.ram_signals) + + + def assert_clock_pulses(self, times=1): + """ Function that asserts we get a specified number of clock pulses. """ + + for _ in range(times): + yield + self.assertEqual((yield self.ram_signals.clk_en), 1) + + + @sync_test_case + def test_register_write(self): + + # Before we transact, CS should be de-asserted, and RWDS and DQ should be undriven. + yield + self.assertEqual((yield self.ram_signals.cs), 0) + self.assertEqual((yield self.ram_signals.dq.e), 0) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + + yield from self.advance_cycles(10) + self.assertEqual((yield self.ram_signals.cs), 0) + + # Request a register write to ID register 0. + yield self.dut.perform_write .eq(1) + yield self.dut.register_space .eq(1) + yield self.dut.address .eq(0x00BBCCDD) + yield self.dut.start_transfer .eq(1) + yield self.dut.final_word .eq(1) + yield self.dut.write_data .eq(0xBEEF) + + # Simulate the RAM requesting a extended latency. + yield self.ram_signals.rwds.i .eq(1) + yield + + # Ensure that upon requesting, CS goes high, and our clock starts low. + yield + self.assertEqual((yield self.ram_signals.cs), 1) + self.assertEqual((yield self.ram_signals.clk_en), 0) + + # Drop our "start request" line somewhere during the transaction; + # so we don't immediately go into the next transfer. + yield self.dut.start_transfer.eq(0) + + # We should then move to shifting out our first command word, + # which means we're driving DQ with the first word of our command. + yield + yield + self.assertEqual((yield self.ram_signals.cs), 1) + self.assertEqual((yield self.ram_signals.clk_en), 1) + self.assertEqual((yield self.ram_signals.dq.e), 1) + self.assertEqual((yield self.ram_signals.dq.o), 0x6017) + + # This should continue until we've shifted out a full command. + yield + self.assertEqual((yield self.ram_signals.dq.o), 0x799B) + yield + self.assertEqual((yield self.ram_signals.dq.o), 0x0005) + + # Check that we've been driving our output this whole time, + # and haven't been driving RWDS. + self.assertEqual((yield self.ram_signals.dq.e), 1) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + yield + + # For a _register_ write, there shouldn't be latency period. + # This means we should continue driving DQ... + self.assertEqual((yield self.ram_signals.dq.e), 1) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + self.assertEqual((yield self.ram_signals.dq.o), 0xBEEF) + + + + @sync_test_case + def test_register_read(self): + + # Before we transact, CS should be de-asserted, and RWDS and DQ should be undriven. + yield + self.assertEqual((yield self.ram_signals.cs), 0) + self.assertEqual((yield self.ram_signals.dq.e), 0) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + + yield from self.advance_cycles(10) + self.assertEqual((yield self.ram_signals.cs), 0) + + # Request a register read of ID register 0. + yield self.dut.perform_write .eq(0) + yield self.dut.register_space .eq(1) + yield self.dut.address .eq(0x00BBCCDD) + yield self.dut.start_transfer .eq(1) + yield self.dut.final_word .eq(1) + + # Simulate the RAM requesting a extended latency. + yield self.ram_signals.rwds.i .eq(1) + yield + + # Ensure that upon requesting, CS goes high, and our clock starts low. + yield + self.assertEqual((yield self.ram_signals.cs), 1) + self.assertEqual((yield self.ram_signals.clk_en), 0) + + # Drop our "start request" line somewhere during the transaction; + # so we don't immediately go into the next transfer. + yield self.dut.start_transfer.eq(0) + + # We should then move to shifting out our first command word, + # which means we're driving DQ with the first word of our command. + yield + yield + self.assertEqual((yield self.ram_signals.cs), 1) + self.assertEqual((yield self.ram_signals.clk_en), 1) + self.assertEqual((yield self.ram_signals.dq.e), 1) + self.assertEqual((yield self.ram_signals.dq.o), 0xe017) + + # This should continue until we've shifted out a full command. + yield + self.assertEqual((yield self.ram_signals.dq.o), 0x799B) + yield + self.assertEqual((yield self.ram_signals.dq.o), 0x0005) + + # Check that we've been driving our output this whole time, + # and haven't been driving RWDS. + self.assertEqual((yield self.ram_signals.dq.e), 1) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + + # Once we finish scanning out the word, we should stop driving + # the data lines, and should finish two latency periods before + # sending any more data. + yield + self.assertEqual((yield self.ram_signals.dq.e), 0) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + self.assertEqual((yield self.ram_signals.clk_en), 1) + + # By this point, the RAM will drive RWDS low. + yield self.ram_signals.rwds.i.eq(0) + + # Ensure the clock still ticking... + yield + self.assertEqual((yield self.ram_signals.clk_en), 1) + + # ... and remains so for the remainder of the latency period. + yield from self.assert_clock_pulses(14) + + # Now, shift in a pair of data words. + yield self.ram_signals.dq.i.eq(0xCAFE) + yield self.ram_signals.rwds.i.eq(0b10) + yield + + # Once this finished, we should have a result on our data out. + self.assertEqual((yield self.dut.read_data), 0xCAFE) + self.assertEqual((yield self.dut.read_ready ), 1) + + yield + yield + self.assertEqual((yield self.ram_signals.cs), 0) + self.assertEqual((yield self.ram_signals.dq.e), 0) + self.assertEqual((yield self.ram_signals.rwds.e), 0) + + # Ensure that our clock drops back to '0' during idle cycles. + yield from self.advance_cycles(2) + self.assertEqual((yield self.ram_signals.clk_en), 0) + + # TODO: test recovery time + diff --git a/tests/test_spi.py b/tests/test_spi.py new file mode 100644 index 000000000..dbe22ebfb --- /dev/null +++ b/tests/test_spi.py @@ -0,0 +1,109 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test.utils import LunaGatewareTestCase, sync_test_case + +from amaranth import Signal +from luna.gateware.interface.spi import SPIDeviceInterface, SPIRegisterInterface, SPIGatewareTestCase + +class SPIDeviceInterfaceTest(SPIGatewareTestCase): + FRAGMENT_UNDER_TEST = SPIDeviceInterface + FRAGMENT_ARGUMENTS = dict(word_size=16, clock_polarity=1) + + def initialize_signals(self): + yield self.dut.spi.cs.eq(0) + + + @sync_test_case + def test_spi_interface(self): + + # Ensure that we don't complete a word while CS is deasserted. + for _ in range(10): + self.assertEqual((yield self.dut.word_complete), 0) + yield + + # Set the word we're expected to send, and then assert CS. + yield self.dut.word_out.eq(0xABCD) + yield + + yield self.dut.spi.cs.eq(1) + yield + + # Verify that the SPI in/out behavior is what we expect. + response = yield from self.spi_exchange_data(b"\xCA\xFE") + self.assertEqual(response, b"\xAB\xCD") + self.assertEqual((yield self.dut.word_in), 0xCAFE) + + + @sync_test_case + def test_spi_transmit_second_word(self): + + # Set the word we're expected to send, and then assert CS. + yield self.dut.word_out.eq(0x0f00) + yield + + yield self.dut.spi.cs.eq(1) + yield + + # Verify that the SPI in/out behavior is what we expect. + response = yield from self.spi_exchange_data(b"\x00\x00") + self.assertEqual(response, b"\x0F\x00") + + + +class SPIRegisterInterfaceTest(SPIGatewareTestCase): + """ Tests for the SPI command interface. """ + + def instantiate_dut(self): + + self.write_strobe = Signal() + + # Create a register and sample dataset to work with. + dut = SPIRegisterInterface(default_read_value=0xDEADBEEF) + dut.add_register(2, write_strobe=self.write_strobe) + + return dut + + + def initialize_signals(self): + # Start off with our clock low and the transaction idle. + yield self.dut.spi.sck.eq(0) + yield self.dut.spi.cs.eq(0) + + + @sync_test_case + def test_undefined_read_behavior(self): + data = yield from self.spi_exchange_data([0, 1, 0, 0, 0, 0]) + self.assertEqual(bytes(data), b"\x00\x00\xde\xad\xbe\xef") + + + @sync_test_case + def test_write_behavior(self): + + # Send a write command... + data = yield from self.spi_exchange_data(b"\x80\x02\x12\x34\x56\x78") + self.assertEqual(bytes(data), b"\x00\x00\x00\x00\x00\x00") + + # ... and then read the relevant data back. + data = yield from self.spi_exchange_data(b"\x00\x02\x12\x34\x56\x78") + self.assertEqual(bytes(data), b"\x00\x00\x12\x34\x56\x78") + + + @sync_test_case + def test_aborted_write_behavior(self): + + # Set an initial value... + data = yield from self.spi_exchange_data(b"\x80\x02\x12\x34\x56\x78") + + # ... and then perform an incomplete write. + data = yield from self.spi_exchange_data(b"\x80\x02\xAA\xBB") + + # We should return to being idle after CS is de-asserted... + yield + self.assertEqual((yield self.dut.idle), 1) + + # ... and our register data should not have changed. + data = yield from self.spi_exchange_data(b"\x00\x02\x12\x34\x56\x78") + self.assertEqual(bytes(data), b"\x00\x00\x12\x34\x56\x78") diff --git a/tests/test_stream_generator.py b/tests/test_stream_generator.py new file mode 100644 index 000000000..be4ed0191 --- /dev/null +++ b/tests/test_stream_generator.py @@ -0,0 +1,276 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaUSBGatewareTestCase, LunaSSGatewareTestCase, ss_domain_test_case, usb_domain_test_case + +from luna.gateware.stream.generator import ConstantStreamGenerator +from luna.gateware.usb.stream import SuperSpeedStreamInterface + +class ConstantStreamGeneratorTest(LunaUSBGatewareTestCase): + FRAGMENT_UNDER_TEST = ConstantStreamGenerator + FRAGMENT_ARGUMENTS = {'constant_data': b"HELLO, WORLD", 'domain': "usb", 'max_length_width': 16} + + @usb_domain_test_case + def test_basic_transmission(self): + dut = self.dut + + # Establish a very high max length; so it doesn't apply. + yield dut.max_length.eq(1000) + + # We shouldn't see a transmission before we request a start. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 0) + self.assertEqual((yield dut.stream.first), 0) + self.assertEqual((yield dut.stream.last), 0) + + # Once we pulse start, we should see the transmission start, + # and we should see our first byte of data. + yield from self.pulse(dut.start) + self.assertEqual((yield dut.stream.valid), 1) + self.assertEqual((yield dut.stream.payload), ord('H')) + self.assertEqual((yield dut.stream.first), 1) + + # That data should remain there until we accept it. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 1) + self.assertEqual((yield dut.stream.payload), ord('H')) + + # Once we indicate that we're accepting data... + yield dut.stream.ready.eq(1) + yield + + # ... we should start seeing the remainder of our transmission. + for i in 'ELLO': + yield + self.assertEqual((yield dut.stream.payload), ord(i)) + self.assertEqual((yield dut.stream.first), 0) + + + # If we drop the 'accepted', we should still see the next byte... + yield dut.stream.ready.eq(0) + yield + self.assertEqual((yield dut.stream.payload), ord(',')) + + # ... but that byte shouldn't be accepted, so we should remain there. + yield + self.assertEqual((yield dut.stream.payload), ord(',')) + + # If we start accepting data again... + yield dut.stream.ready.eq(1) + yield + + # ... we should see the remainder of the stream. + for i in ' WORLD': + yield + self.assertEqual((yield dut.stream.payload), ord(i)) + + + # On the last byte of data, we should see last = 1. + self.assertEqual((yield dut.stream.last), 1) + + # After the last datum, we should see valid drop to '0'. + yield + self.assertEqual((yield dut.stream.valid), 0) + + @usb_domain_test_case + def test_basic_start_position(self): + dut = self.dut + + # Start at position 2 + yield dut.start_position.eq(2) + + # Establish a very high max length; so it doesn't apply. + yield dut.max_length.eq(1000) + + # We shouldn't see a transmission before we request a start. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 0) + self.assertEqual((yield dut.stream.first), 0) + self.assertEqual((yield dut.stream.last), 0) + + # Once we pulse start, we should see the transmission start, + # and we should see our first byte of data. + yield from self.pulse(dut.start) + self.assertEqual((yield dut.stream.valid), 1) + self.assertEqual((yield dut.stream.payload), ord('L')) + self.assertEqual((yield dut.stream.first), 1) + + # That data should remain there until we accept it. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 1) + self.assertEqual((yield dut.stream.payload), ord('L')) + + # Once we indicate that we're accepting data... + yield dut.stream.ready.eq(1) + yield + + # ... we should start seeing the remainder of our transmission. + for i in 'LO': + yield + self.assertEqual((yield dut.stream.payload), ord(i)) + self.assertEqual((yield dut.stream.first), 0) + + + # If we drop the 'accepted', we should still see the next byte... + yield dut.stream.ready.eq(0) + yield + self.assertEqual((yield dut.stream.payload), ord(',')) + + # ... but that byte shouldn't be accepted, so we should remain there. + yield + self.assertEqual((yield dut.stream.payload), ord(',')) + + # If we start accepting data again... + yield dut.stream.ready.eq(1) + yield + + # ... we should see the remainder of the stream. + for i in ' WORLD': + yield + self.assertEqual((yield dut.stream.payload), ord(i)) + + + # On the last byte of data, we should see last = 1. + self.assertEqual((yield dut.stream.last), 1) + + # After the last datum, we should see valid drop to '0'. + yield + self.assertEqual((yield dut.stream.valid), 0) + + @usb_domain_test_case + def test_max_length(self): + dut = self.dut + + yield dut.stream.ready.eq(1) + yield dut.max_length.eq(6) + + # Once we pulse start, we should see the transmission start, + yield from self.pulse(dut.start) + + # ... we should start seeing the remainder of our transmission. + for i in 'HELLO': + self.assertEqual((yield dut.stream.payload), ord(i)) + yield + + + # On the last byte of data, we should see last = 1. + self.assertEqual((yield dut.stream.last), 1) + + # After the last datum, we should see valid drop to '0'. + yield + self.assertEqual((yield dut.stream.valid), 0) + + + +class ConstantStreamGeneratorWideTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = ConstantStreamGenerator + FRAGMENT_ARGUMENTS = dict( + domain = "ss", + constant_data = b"HELLO WORLD", + stream_type = SuperSpeedStreamInterface, + max_length_width = 16 + ) + + + @ss_domain_test_case + def test_basic_transmission(self): + dut = self.dut + + # Establish a very high max length; so it doesn't apply. + yield dut.max_length.eq(1000) + + # We shouldn't see a transmission before we request a start. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 0) + self.assertEqual((yield dut.stream.first), 0) + self.assertEqual((yield dut.stream.last), 0) + + # Once we pulse start, we should see the transmission start, + # and we should see our first byte of data. + yield from self.pulse(dut.start) + self.assertEqual((yield dut.stream.valid), 0b1111) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) + self.assertEqual((yield dut.stream.first), 1) + + # That data should remain there until we accept it. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.stream.valid), 0b1111) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) + + # Once we indicate that we're accepting data... + yield dut.stream.ready.eq(1) + yield + + # ... we should start seeing the remainder of our transmission. + yield + self.assertEqual((yield dut.stream.valid), 0b1111) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"O WO", byteorder="little")) + self.assertEqual((yield dut.stream.first), 0) + + + yield + self.assertEqual((yield dut.stream.valid), 0b111) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"RLD", byteorder="little")) + self.assertEqual((yield dut.stream.first), 0) + + + # On the last byte of data, we should see last = 1. + self.assertEqual((yield dut.stream.last), 1) + + # After the last datum, we should see valid drop to '0'. + yield + self.assertEqual((yield dut.stream.valid), 0) + + + @ss_domain_test_case + def test_max_length_transmission(self): + dut = self.dut + + # Apply a maximum length of six bytes. + yield dut.max_length.eq(6) + yield dut.stream.ready.eq(1) + + # Once we pulse start, we should see the transmission start, + # and we should see our first byte of data. + yield from self.pulse(dut.start) + self.assertEqual((yield dut.stream.valid), 0b1111) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) + self.assertEqual((yield dut.stream.first), 1) + + # We should then see only two bytes of our remainder. + yield + self.assertEqual((yield dut.stream.valid), 0b0011) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"O WO", byteorder="little")) + self.assertEqual((yield dut.stream.first), 0) + self.assertEqual((yield dut.stream.last), 1) + + + @ss_domain_test_case + def test_very_short_max_length(self): + dut = self.dut + + # Apply a maximum length of six bytes. + yield dut.max_length.eq(2) + + # Once we pulse start, we should see the transmission start, + # and we should see our first word of data. + yield from self.pulse(dut.start) + self.assertEqual((yield dut.stream.valid), 0b0011) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) + self.assertEqual((yield dut.stream.first), 1) + self.assertEqual((yield dut.stream.last), 1) + + # Our data should remain there until it's accepted. + yield dut.stream.ready.eq(1) + yield + self.assertEqual((yield dut.stream.valid), 0b0011) + self.assertEqual((yield dut.stream.payload), int.from_bytes(b"HELL", byteorder="little")) + self.assertEqual((yield dut.stream.first), 1) + self.assertEqual((yield dut.stream.last), 1) + + # After acceptance, valid should drop back to false. + yield + self.assertEqual((yield dut.stream.valid), 0b0000) + diff --git a/tests/test_uart.py b/tests/test_uart.py new file mode 100644 index 000000000..a18bf48fd --- /dev/null +++ b/tests/test_uart.py @@ -0,0 +1,140 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test.utils import LunaGatewareTestCase, sync_test_case + +from luna.gateware.interface.uart import UARTMultibyteTransmitter, UARTTransmitter + +class UARTTransmitterTest(LunaGatewareTestCase): + DIVISOR = 10 + + FRAGMENT_UNDER_TEST = UARTTransmitter + FRAGMENT_ARGUMENTS = dict(divisor=DIVISOR) + + + def advance_half_bit(self): + yield from self.advance_cycles(self.DIVISOR // 2) + + def advance_bit(self): + yield from self.advance_cycles(self.DIVISOR) + + + def assert_data_sent(self, byte_expected): + dut = self.dut + + # Our start bit should remain present until the next bit period. + yield from self.advance_half_bit() + self.assertEqual((yield dut.tx), 0) + + # We should then see each bit of our data, LSB first. + bits = [int(i) for i in f"{byte_expected:08b}"] + for bit in bits[::-1]: + yield from self.advance_bit() + self.assertEqual((yield dut.tx), bit) + + # Finally, we should see a stop bit. + yield from self.advance_bit() + self.assertEqual((yield dut.tx), 1) + + + @sync_test_case + def test_burst_transmit(self): + dut = self.dut + stream = dut.stream + + # We should remain idle until a transmit is requested... + yield from self.advance_cycles(10) + self.assertEqual((yield dut.idle), 1) + self.assertEqual((yield dut.stream.ready), 1) + + # ... and our tx line should idle high. + self.assertEqual((yield dut.tx), 1) + + # First, transmit 0x55 (maximum transition rate). + yield stream.payload.eq(0x55) + yield stream.valid.eq(1) + + # We should see our data become accepted; and we + # should see a start bit. + yield + self.assertEqual((yield stream.ready), 1) + yield + self.assertEqual((yield dut.tx), 0) + + # Provide our next byte of data once the current + # one has been accepted. Changing this before the tests + # below ensures that we validate that data is latched properly. + yield stream.payload.eq(0x66) + + # Ensure we get our data correctly. + yield from self.assert_data_sent(0x55) + yield from self.assert_data_sent(0x66) + + # Stop transmitting after the next frame. + yield stream.valid.eq(0) + + # Ensure we actually stop. + yield from self.advance_bit() + self.assertEqual((yield dut.idle), 1) + + +class UARTMultibyteTransmitterTest(LunaGatewareTestCase): + DIVISOR = 10 + + FRAGMENT_UNDER_TEST = UARTMultibyteTransmitter + FRAGMENT_ARGUMENTS = dict(divisor=DIVISOR, byte_width=4) + + + def advance_half_bit(self): + yield from self.advance_cycles(self.DIVISOR // 2) + + def advance_bit(self): + yield from self.advance_cycles(self.DIVISOR) + + + def assert_data_sent(self, byte_expected): + dut = self.dut + + # Our start bit should remain present until the next bit period. + yield from self.advance_half_bit() + self.assertEqual((yield dut.tx), 0) + + # We should then see each bit of our data, LSB first. + bits = [int(i) for i in f"{byte_expected:08b}"] + for bit in bits[::-1]: + yield from self.advance_bit() + self.assertEqual((yield dut.tx), bit) + + # Finally, we should see a stop bit. + yield from self.advance_bit() + self.assertEqual((yield dut.tx), 1) + + yield from self.advance_cycles(2) + + + @sync_test_case + def test_burst_transmit(self): + dut = self.dut + stream = dut.stream + + # We should remain idle until a transmit is requested... + yield from self.advance_cycles(10) + self.assertEqual((yield dut.idle), 1) + self.assertEqual((yield dut.stream.ready), 1) + + # Transmit a four-byte word. + yield stream.payload.eq(0x11223355) + yield stream.valid.eq(1) + + # We should see our data become accepted; and we + # should see a start bit. + yield + self.assertEqual((yield stream.ready), 1) + + # Ensure we get our data correctly, and that our transmitter + # isn't accepting data mid-frame. + yield from self.assert_data_sent(0x55) + self.assertEqual((yield stream.ready), 0) + yield from self.assert_data_sent(0x33) diff --git a/tests/test_ulpi.py b/tests/test_ulpi.py new file mode 100644 index 000000000..385ee5dde --- /dev/null +++ b/tests/test_ulpi.py @@ -0,0 +1,388 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test.utils import LunaGatewareTestCase, usb_domain_test_case + +from amaranth import Record +from luna.gateware.interface.ulpi import ULPIControlTranslator, ULPIRegisterWindow, ULPIRxEventDecoder, ULPITransmitTranslator + +class TestULPIRegisters(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = ULPIRegisterWindow + + USB_CLOCK_FREQUENCY = 60e6 + SYNC_CLOCK_FREQUENCY = None + + def initialize_signals(self): + yield self.dut.ulpi_dir.eq(0) + + yield self.dut.read_request.eq(0) + yield self.dut.write_request.eq(0) + + + @usb_domain_test_case + def test_idle_behavior(self): + """ Ensure we apply a NOP whenever we're not actively performing a command. """ + self.assertEqual((yield self.dut.ulpi_data_out), 0) + + + @usb_domain_test_case + def test_register_read(self): + """ Validates a register read. """ + + # Poison the register value with a fail value (0xBD). + yield self.dut.ulpi_data_in.eq(0xBD) + + # Set up a read request. + yield self.dut.address.eq(0) + yield + + # After a read request, we should be busy... + yield from self.pulse(self.dut.read_request) + self.assertEqual((yield self.dut.busy), 1) + + # ... and then, since dir is unasserted, we should have a read command. + yield + self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) + + # We should continue to present the command... + yield from self.advance_cycles(10) + self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) + self.assertEqual((yield self.dut.busy), 1) + + # ... until the host accepts it. + yield self.dut.ulpi_next.eq(1) + yield + + # We should then wait for a single bus turnaround cycle before reading. + yield + + # And then should read whatever value is present. + yield self.dut.ulpi_data_in.eq(0x07) + yield + yield + self.assertEqual((yield self.dut.read_data), 0x07) + + # Finally, we should return to idle. + self.assertEqual((yield self.dut.busy), 0) + + + @usb_domain_test_case + def test_interrupted_read(self): + """ Validates how a register read works when interrupted by a change in DIR. """ + + # Set up a read request while DIR is asserted. + yield self.dut.ulpi_dir.eq(1) + yield self.dut.address.eq(0) + yield from self.pulse(self.dut.read_request) + + # We shouldn't try to output anything until DIR is de-asserted. + yield from self.advance_cycles(1) + self.assertEqual((yield self.dut.ulpi_out_req), 0) + yield from self.advance_cycles(10) + self.assertEqual((yield self.dut.ulpi_out_req), 0) + + # De-assert DIR, and let the platform apply a read command. + yield self.dut.ulpi_dir.eq(0) + yield from self.advance_cycles(2) + self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) + + # Assert DIR again; interrupting the read. This should bring + # the platform back to its "waiting for the bus" state. + yield self.dut.ulpi_dir.eq(1) + yield from self.advance_cycles(2) + self.assertEqual((yield self.dut.ulpi_out_req), 0) + + # Clear DIR, and validate that the device starts driving the command again + yield self.dut.ulpi_dir.eq(0) + yield from self.advance_cycles(2) + self.assertEqual((yield self.dut.ulpi_data_out), 0b11000000) + + # Apply NXT so the read can finally continue. + yield self.dut.ulpi_next.eq(1) + yield + + # We should then wait for a single bus turnaround cycle before reading. + yield + + # And then should read whatever value is present. + yield self.dut.ulpi_data_in.eq(0x07) + yield + yield + self.assertEqual((yield self.dut.read_data), 0x07) + + # Finally, we should return to idle. + self.assertEqual((yield self.dut.busy), 0) + + + @usb_domain_test_case + def test_register_write(self): + + # Set up a write request. + yield self.dut.address.eq(0b10) + yield self.dut.write_data.eq(0xBC) + yield + + # Starting the request should make us busy. + yield from self.pulse(self.dut.write_request) + self.assertEqual((yield self.dut.busy), 1) + + # ... and then, since dir is unasserted, we should have a write command. + yield + self.assertEqual((yield self.dut.ulpi_data_out), 0b10000010) + + # We should continue to present the command... + yield from self.advance_cycles(10) + self.assertEqual((yield self.dut.ulpi_data_out), 0b10000010) + self.assertEqual((yield self.dut.busy), 1) + + # ... until the host accepts it. + yield self.dut.ulpi_next.eq(1) + yield + + # We should then present the data to be written... + yield self.dut.ulpi_next.eq(0) + yield + self.assertEqual((yield self.dut.ulpi_data_out), 0xBC) + + # ... and continue doing so until the host accepts it... + yield from self.advance_cycles(10) + self.assertEqual((yield self.dut.ulpi_data_out), 0xBC) + + yield self.dut.ulpi_next.eq(1) + yield from self.advance_cycles(2) + + # ... at which point stop should be asserted for one cycle. + self.assertEqual((yield self.dut.ulpi_stop), 1) + yield + + # Finally, we should go idle. + self.assertEqual((yield self.dut.ulpi_stop), 0) + self.assertEqual((yield self.dut.busy), 0) + + +class ULPIRxEventDecoderTest(LunaGatewareTestCase): + + USB_CLOCK_FREQUENCY = 60e6 + SYNC_CLOCK_FREQUENCY = None + + def instantiate_dut(self): + + self.ulpi = Record([ + ("dir", [ + ("i", 1), + ]), + ("nxt", [ + ("i", 1), + ]), + ("data", [ + ("i", 8), + ]) + ]) + + return ULPIRxEventDecoder(ulpi_bus=self.ulpi) + + + def initialize_signals(self): + yield self.ulpi.dir.i.eq(0) + yield self.ulpi.nxt.i.eq(0) + yield self.ulpi.data.i.eq(0) + yield self.dut.register_operation_in_progress.eq(0) + + + @usb_domain_test_case + def test_decode(self): + + # Provide a test value. + yield self.ulpi.data.i.eq(0xAB) + + # First, set DIR and NXT at the same time, and verify that we + # don't register an RxEvent. + yield self.ulpi.dir.i.eq(1) + yield self.ulpi.nxt.i.eq(1) + + yield from self.advance_cycles(5) + self.assertEqual((yield self.dut.last_rx_command), 0x00) + + # Nothing should change when we drop DIR and NXT. + yield self.ulpi.dir.i.eq(0) + yield self.ulpi.nxt.i.eq(0) + yield + self.assertEqual((yield self.dut.last_rx_command), 0x00) + + + # Setting DIR but not NXT should trigger an RxEvent; but not + # until one cycle of "bus turnaround" has passed. + yield self.ulpi.dir.i.eq(1) + + yield self.ulpi.data.i.eq(0x12) + yield + self.assertEqual((yield self.dut.last_rx_command), 0x00) + + yield self.ulpi.data.i.eq(0b00011110) + yield from self.advance_cycles(2) + + self.assertEqual((yield self.dut.last_rx_command), 0b00011110) + + # Validate that we're decoding this RxCommand correctly. + self.assertEqual((yield self.dut.line_state), 0b10) + self.assertEqual((yield self.dut.vbus_valid), 1) + self.assertEqual((yield self.dut.rx_active), 1) + self.assertEqual((yield self.dut.rx_error), 0) + self.assertEqual((yield self.dut.host_disconnect), 0) + + +class ControlTranslatorTest(LunaGatewareTestCase): + + USB_CLOCK_FREQUENCY = 60e6 + SYNC_CLOCK_FREQUENCY = None + + def instantiate_dut(self): + self.reg_window = ULPIRegisterWindow() + return ULPIControlTranslator(register_window=self.reg_window, own_register_window=True) + + + def initialize_signals(self): + dut = self.dut + + # Initialize our register signals to their default values. + yield dut.xcvr_select.eq(1) + yield dut.dm_pulldown.eq(1) + yield dut.dp_pulldown.eq(1) + yield dut.use_external_vbus_indicator.eq(0) + yield dut.bus_idle.eq(1) + + + @usb_domain_test_case + def test_multiwrite_behavior(self): + + # Give our initialization some time to settle, + # and verify that we haven't initiated anyting in that interim. + yield from self.advance_cycles(10) + self.assertEqual((yield self.reg_window.write_request), 0) + + # Change signals that span two registers. + yield self.dut.op_mode.eq(0b11) + yield self.dut.dp_pulldown.eq(0) + yield self.dut.dm_pulldown.eq(0) + yield + yield + + # Once we've changed these, we should start trying to apply + # our new value to the function control register. + self.assertEqual((yield self.reg_window.address), 0x04) + self.assertEqual((yield self.reg_window.write_data), 0b01011001) + + # which should occur until the data and address are accepted. + yield self.reg_window.ulpi_next.eq(1) + yield from self.wait_until(self.reg_window.done, timeout=10) + yield + yield + + # We should then experience a write to the function control register. + self.assertEqual((yield self.reg_window.address), 0x0A) + self.assertEqual((yield self.reg_window.write_data), 0b00000000) + + # Wait for that action to complete.. + yield self.reg_window.ulpi_next.eq(1) + yield from self.wait_until(self.reg_window.done, timeout=10) + yield + yield + + # After which we shouldn't be trying to write anything at all. + self.assertEqual((yield self.reg_window.address), 0) + self.assertEqual((yield self.reg_window.write_data), 0) + self.assertEqual((yield self.reg_window.write_request), 0) + + +class ULPITransmitTranslatorTest(LunaGatewareTestCase): + USB_CLOCK_FREQUENCY=60e6 + SYNC_CLOCK_FREQUENCY=None + + FRAGMENT_UNDER_TEST = ULPITransmitTranslator + + def initialize_signals(self): + yield self.dut.bus_idle.eq(1) + + @usb_domain_test_case + def test_simple_transmit(self): + dut = self.dut + + # We shouldn't try to transmit until we have a transmit request. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.ulpi_out_req), 0) + + # Present a simple SOF PID. + yield dut.tx_valid.eq(1) + yield dut.tx_data.eq(0xA5) + yield + + # Our PID should have been translated into a transmit request, with + # our PID in the lower nibble. + self.assertEqual((yield dut.ulpi_data_out), 0b01000101) + self.assertEqual((yield dut.tx_ready), 0) + self.assertEqual((yield dut.ulpi_stp), 0) + yield + self.assertEqual((yield dut.ulpi_out_req), 1) + + # Our PID should remain there until we indicate we're ready. + self.advance_cycles(10) + self.assertEqual((yield dut.ulpi_data_out), 0b01000101) + + # Once we're ready, we should accept the data from the link and continue. + yield dut.ulpi_nxt.eq(1) + yield + self.assertEqual((yield dut.tx_ready), 1) + yield dut.tx_data.eq(0x11) + yield + + # At which point we should present the relevant data directly. + yield + self.assertEqual((yield dut.ulpi_data_out), 0x11) + + # Finally, once we stop our transaction... + yield dut.tx_valid.eq(0) + yield + + # ... we should get a cycle of STP. + self.assertEqual((yield dut.ulpi_data_out), 0) + self.assertEqual((yield dut.ulpi_stp), 1) + + # ... followed by idle. + yield + self.assertEqual((yield dut.ulpi_stp), 0) + + + @usb_domain_test_case + def test_handshake(self): + dut = self.dut + + # Present a simple ACK PID. + yield dut.tx_valid.eq(1) + yield dut.tx_data.eq(0b11010010) + yield + + # Our PID should have been translated into a transmit request, with + # our PID in the lower nibble. + self.assertEqual((yield dut.ulpi_data_out), 0b01000010) + self.assertEqual((yield dut.tx_ready), 0) + self.assertEqual((yield dut.ulpi_stp), 0) + + # Once the PHY accepts the data, it'll assert NXT. + yield dut.ulpi_nxt.eq(1) + yield + self.assertEqual((yield dut.ulpi_out_req), 1) + + # ... which will trigger the transmitter to drop tx_valid. + yield dut.tx_valid.eq(0) + + # ... we should get a cycle of STP. + yield + #self.assertEqual((yield dut.ulpi_data_out), 0) + #self.assertEqual((yield dut.ulpi_stp), 1) + + # ... followed by idle. + yield + self.assertEqual((yield dut.ulpi_stp), 0) + diff --git a/tests/test_usb2_descriptor.py b/tests/test_usb2_descriptor.py new file mode 100644 index 000000000..618dae04f --- /dev/null +++ b/tests/test_usb2_descriptor.py @@ -0,0 +1,161 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaUSBGatewareTestCase, usb_domain_test_case + +from luna.gateware.usb.usb2.descriptor import GetDescriptorHandlerBlock, DeviceDescriptorCollection, StandardDescriptorNumbers + +class GetDescriptorHandlerBlockTest(LunaUSBGatewareTestCase): + descriptors = DeviceDescriptorCollection() + + with descriptors.DeviceDescriptor() as d: + d.bcdUSB = 2.00 + d.idVendor = 0x1234 + d.idProduct = 0x4567 + d.iManufacturer = "Manufacturer" + d.iProduct = "Product" + d.iSerialNumber = "ThisSerialNumberIsResultsInADescriptorLongerThan64Bytes" + d.bNumConfigurations = 1 + + with descriptors.ConfigurationDescriptor() as c: + c.bmAttributes = 0xC0 + c.bMaxPower = 50 + + with c.InterfaceDescriptor() as i: + i.bInterfaceNumber = 0 + i.bInterfaceClass = 0x02 + i.bInterfaceSubclass = 0x02 + i.bInterfaceProtocol = 0x01 + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x81 + e.bmAttributes = 0x03 + e.wMaxPacketSize = 64 + e.bInterval = 11 + + # HID Descriptor (Example E.8 of HID specification) + descriptors.add_descriptor(b'\x09\x21\x01\x01\x00\x01\x22\x00\x32') + + FRAGMENT_UNDER_TEST = GetDescriptorHandlerBlock + FRAGMENT_ARGUMENTS = {"descriptor_collection": descriptors} + + def traces_of_interest(self): + dut = self.dut + return (dut.value, dut.length, dut.start_position, dut.start, dut.stall, + dut.tx.ready, dut.tx.first, dut.tx.last, dut.tx.payload, dut.tx.valid) + + def _test_descriptor(self, type_number, index, raw_descriptor, start_position, max_length, delay_ready=0): + """ Triggers a read and checks if correct data is transmitted. """ + + # Set a defined start before starting + yield self.dut.tx.ready.eq(0) + yield + + # Set up request + yield self.dut.value.word_select(1, 8).eq(type_number) # Type + yield self.dut.value.word_select(0, 8).eq(index) # Index + yield self.dut.length.eq(max_length) + yield self.dut.start_position.eq(start_position) + yield self.dut.tx.ready.eq(1 if delay_ready == 0 else 0) + yield self.dut.start.eq(1) + yield + + yield self.dut.start.eq(0) + + yield from self.wait_until(self.dut.tx.valid, timeout=100) + + if delay_ready > 0: + for _ in range(delay_ready-1): + yield + yield self.dut.tx.ready.eq(1) + yield + + max_packet_length = 64 + expected_data = raw_descriptor[start_position:] + expected_bytes = min(len(expected_data), max_length-start_position, max_packet_length) + + if expected_bytes == 0: + self.assertEqual((yield self.dut.tx.first), 0) + self.assertEqual((yield self.dut.tx.last), 1) + self.assertEqual((yield self.dut.tx.valid), 1) + self.assertEqual((yield self.dut.stall), 0) + yield + + else: + for i in range(expected_bytes): + self.assertEqual((yield self.dut.tx.first), 1 if (i == 0) else 0) + self.assertEqual((yield self.dut.tx.last), 1 if (i == expected_bytes - 1) else 0) + self.assertEqual((yield self.dut.tx.valid), 1) + self.assertEqual((yield self.dut.tx.payload), expected_data[i]) + self.assertEqual((yield self.dut.stall), 0) + yield + + self.assertEqual((yield self.dut.tx.valid), 0) + + def _test_stall(self, type_number, index, start_position, max_length): + """ Triggers a read and checks if correctly stalled. """ + + yield self.dut.value.word_select(1, 8).eq(type_number) # Type + yield self.dut.value.word_select(0, 8).eq(index) # Index + yield self.dut.length.eq(max_length) + yield self.dut.start_position.eq(start_position) + yield self.dut.tx.ready.eq(1) + yield self.dut.start.eq(1) + yield + + yield self.dut.start.eq(0) + + cycles_passed = 0 + timeout = 100 + + while not (yield self.dut.stall): + self.assertEqual((yield self.dut.tx.valid), 0) + yield + + cycles_passed += 1 + if timeout and cycles_passed > timeout: + raise RuntimeError(f"Timeout waiting for stall!") + + @usb_domain_test_case + def test_all_descriptors(self): + for type_number, index, raw_descriptor in self.descriptors: + yield from self._test_descriptor(type_number, index, raw_descriptor, 0, len(raw_descriptor)) + yield from self._test_descriptor(type_number, index, raw_descriptor, 0, len(raw_descriptor), delay_ready=10) + + @usb_domain_test_case + def test_all_descriptors_with_offset(self): + for type_number, index, raw_descriptor in self.descriptors: + if len(raw_descriptor) > 1: + yield from self._test_descriptor(type_number, index, raw_descriptor, 1, len(raw_descriptor)) + + @usb_domain_test_case + def test_all_descriptors_with_length(self): + for type_number, index, raw_descriptor in self.descriptors: + if len(raw_descriptor) > 1: + yield from self._test_descriptor(type_number, index, raw_descriptor, 0, min(8, len(raw_descriptor)-1)) + yield from self._test_descriptor(type_number, index, raw_descriptor, 0, min(8, len(raw_descriptor)-1), delay_ready=10) + + @usb_domain_test_case + def test_all_descriptors_with_offset_and_length(self): + for type_number, index, raw_descriptor in self.descriptors: + if len(raw_descriptor) > 1: + yield from self._test_descriptor(type_number, index, raw_descriptor, 1, min(8, len(raw_descriptor)-1)) + + @usb_domain_test_case + def test_all_descriptors_with_zero_length(self): + for type_number, index, raw_descriptor in self.descriptors: + yield from self._test_descriptor(type_number, index, raw_descriptor, 0, 0) + + @usb_domain_test_case + def test_unavailable_descriptor(self): + yield from self._test_stall(StandardDescriptorNumbers.STRING, 100, 0, 64) + + @usb_domain_test_case + def test_unavailable_index_type(self): + # Unavailable index in between + yield from self._test_stall(0x10, 0, 0, 64) + + # Index after last used type + yield from self._test_stall(0x42, 0, 0, 64) diff --git a/tests/test_usb2_device.py b/tests/test_usb2_device.py new file mode 100644 index 000000000..1d5f005c1 --- /dev/null +++ b/tests/test_usb2_device.py @@ -0,0 +1,219 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import usb_domain_test_case +from luna.gateware.test.usb2 import USBDeviceTest + +from luna.gateware.usb.usb2 import USBPacketID +from luna.gateware.usb.usb2.device import USBDevice + +from usb_protocol.emitters import DeviceDescriptorCollection +from usb_protocol.types import DescriptorTypes + +class FullDeviceTest(USBDeviceTest): + """ :meta private: """ + + FRAGMENT_UNDER_TEST = USBDevice + FRAGMENT_ARGUMENTS = {'handle_clocking': False} + + def traces_of_interest(self): + return ( + self.utmi.tx_data, + self.utmi.tx_valid, + self.utmi.rx_data, + self.utmi.rx_valid, + ) + + def initialize_signals(self): + + # Keep our device from resetting. + yield self.utmi.line_state.eq(0b01) + + # Have our USB device connected. + yield self.dut.connect.eq(1) + + # Pretend our PHY is always ready to accept data, + # so we can move forward quickly. + yield self.utmi.tx_ready.eq(1) + + + def provision_dut(self, dut): + self.descriptors = descriptors = DeviceDescriptorCollection() + + with descriptors.DeviceDescriptor() as d: + d.idVendor = 0x16d0 + d.idProduct = 0xf3b + + d.iManufacturer = "LUNA" + d.iProduct = "Test Device" + d.iSerialNumber = "1234" + + d.bNumConfigurations = 1 + + # Provide a core configuration descriptor for testing. + with descriptors.ConfigurationDescriptor() as c: + + with c.InterfaceDescriptor() as i: + i.bInterfaceNumber = 0 + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x01 + e.wMaxPacketSize = 512 + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x81 + e.wMaxPacketSize = 512 + + dut.add_standard_control_endpoint(descriptors) + + + @usb_domain_test_case + def test_enumeration(self): + + # Reference enumeration process (quirks merged from Linux, macOS, and Windows): + # - Read 8 bytes of device descriptor. + # - Read 64 bytes of device descriptor. + # - Set address. + # - Read exact device descriptor length. + # - Read device qualifier descriptor, three times. + # - Read config descriptor (without subordinates). + # - Read language descriptor. + # - Read Windows extended descriptors. [optional] + # - Read string descriptors from device descriptor (wIndex=language id). + # - Set configuration. + # - Read back configuration number and validate. + + + # Read 8 bytes of our device descriptor. + handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=8) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)[0:8]) + + # Read 64 bytes of our device descriptor, no matter its length. + handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=64) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)) + + # Send a nonsense request, and validate that it's stalled. + handshake, data = yield from self.control_request_in(0x80, 30, length=10) + self.assertEqual(handshake, USBPacketID.STALL) + + # Send a set-address request; we'll apply an arbitrary address 0x31. + yield from self.set_address(0x31) + self.assertEqual(self.address, 0x31) + + # Read our device descriptor. + handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE, length=18) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.DEVICE)) + + # Read our device qualifier descriptor. + for _ in range(3): + handshake, data = yield from self.get_descriptor(DescriptorTypes.DEVICE_QUALIFIER, length=10) + self.assertEqual(handshake, USBPacketID.STALL) + + # Read our configuration descriptor (no subordinates). + handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=9) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION)[0:9]) + + # Read our configuration descriptor (with subordinates). + handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=32) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION)) + + # Read our string descriptors. + for i in range(4): + handshake, data = yield from self.get_descriptor(DescriptorTypes.STRING, index=i, length=255) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), self.descriptors.get_descriptor_bytes(DescriptorTypes.STRING, index=i)) + + # Set our configuration... + status_pid = yield from self.set_configuration(1) + self.assertEqual(status_pid, USBPacketID.DATA1) + + # ... and ensure it's applied. + handshake, configuration = yield from self.get_configuration() + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(configuration, [1], "device did not accept configuration!") + + +class LongDescriptorTest(USBDeviceTest): + """ :meta private: """ + + FRAGMENT_UNDER_TEST = USBDevice + FRAGMENT_ARGUMENTS = {'handle_clocking': False} + + def initialize_signals(self): + + # Keep our device from resetting. + yield self.utmi.line_state.eq(0b01) + + # Have our USB device connected. + yield self.dut.connect.eq(1) + + # Pretend our PHY is always ready to accept data, + # so we can move forward quickly. + yield self.utmi.tx_ready.eq(1) + + + def provision_dut(self, dut): + self.descriptors = descriptors = DeviceDescriptorCollection() + + with descriptors.DeviceDescriptor() as d: + d.idVendor = 0x16d0 + d.idProduct = 0xf3b + + d.iManufacturer = "LUNA" + d.iProduct = "Test Device" + d.iSerialNumber = "1234" + + d.bNumConfigurations = 1 + + # Provide a core configuration descriptor for testing. + with descriptors.ConfigurationDescriptor() as c: + + with c.InterfaceDescriptor() as i: + i.bInterfaceNumber = 0 + + for n in range(15): + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = n + e.wMaxPacketSize = 512 + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x80 | n + e.wMaxPacketSize = 512 + + dut.add_standard_control_endpoint(descriptors) + + @usb_domain_test_case + def test_long_descriptor(self): + descriptor = self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION) + + # Read our configuration descriptor (no subordinates). + handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=len(descriptor)) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), descriptor) + self.assertEqual(len(data), len(descriptor)) + + @usb_domain_test_case + def test_descriptor_zlp(self): + # Try requesting a long descriptor, but using a length that is a + # multiple of the endpoint's maximum packet length. This should cause + # the device to return some number of packets with the maximum packet + # length, followed by a zero-length packet to terminate the + # transaction. + + descriptor = self.descriptors.get_descriptor_bytes(DescriptorTypes.CONFIGURATION) + + # Try requesting a single and three max-sized packet. + for factor in [1, 3]: + request_length = self.max_packet_size_ep0 * factor + handshake, data = yield from self.get_descriptor(DescriptorTypes.CONFIGURATION, length=request_length) + self.assertEqual(handshake, USBPacketID.ACK) + self.assertEqual(bytes(data), descriptor[0:request_length]) + self.assertEqual(len(data), request_length) diff --git a/tests/test_usb2_packet.py b/tests/test_usb2_packet.py new file mode 100644 index 000000000..4643a416d --- /dev/null +++ b/tests/test_usb2_packet.py @@ -0,0 +1,554 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, usb_domain_test_case + +from amaranth import Record +from luna.gateware.usb.usb2.packet import USBDataPacketDeserializer, USBDataPacketGenerator, USBDataPacketReceiver +from luna.gateware.usb.usb2.packet import USBHandshakeDetector, USBHandshakeGenerator +from luna.gateware.usb.usb2.packet import InterpacketTimerInterface, USBInterpacketTimer, USBTokenDetector + +class USBPacketizerTest(LunaGatewareTestCase): + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + def instantiate_dut(self, extra_arguments=None): + self.utmi = Record([ + ("rx_data", 8), + ("rx_active", 1), + ("rx_valid", 1) + ]) + + # If we don't have explicit extra arguments, use the base class's. + if extra_arguments is None: + extra_arguments = self.FRAGMENT_ARGUMENTS + + return self.FRAGMENT_UNDER_TEST(utmi=self.utmi, **extra_arguments) + + def provide_byte(self, byte): + """ Provides a given byte on the UTMI receive data for one cycle. """ + yield self.utmi.rx_data.eq(byte) + yield + + + def start_packet(self, *, set_rx_valid=True): + """ Starts a UTMI packet receive. """ + yield self.utmi.rx_active.eq(1) + + if set_rx_valid: + yield self.utmi.rx_valid.eq(1) + + yield + + + def end_packet(self): + """ Starts a UTMI packet receive. """ + yield self.utmi.rx_active.eq(0) + yield self.utmi.rx_valid.eq(0) + yield + + + def provide_packet(self, *octets, cycle_after=True): + """ Provides an entire packet transaction at once; for convenience. """ + yield from self.start_packet() + for b in octets: + yield from self.provide_byte(b) + yield from self.end_packet() + + if cycle_after: + yield + + +class USBTokenDetectorTest(USBPacketizerTest): + FRAGMENT_UNDER_TEST = USBTokenDetector + + @usb_domain_test_case + def test_valid_token(self): + dut = self.dut + + # Assume our device is at address 0x3a. + yield dut.address.eq(0x3a) + + # When idle, we should have no new-packet events. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.interface.new_frame), 0) + self.assertEqual((yield dut.interface.new_token), 0) + + # From: https://usb.org/sites/default/files/crcdes.pdf + # out to 0x3a, endpoint 0xa => 0xE1 5C BC + yield from self.provide_packet(0b11100001, 0b00111010, 0b00111101) + + # Validate that we just finished a token. + self.assertEqual((yield dut.interface.new_token), 1) + self.assertEqual((yield dut.interface.new_frame), 0) + + # Validate that we got the expected PID. + self.assertEqual((yield dut.interface.pid), 0b0001) + + # Validate that we got the expected address / endpoint. + self.assertEqual((yield dut.interface.address), 0x3a) + self.assertEqual((yield dut.interface.endpoint), 0xa ) + + # Ensure that our strobe returns to 0, afterwards. + yield + self.assertEqual((yield dut.interface.new_token), 0) + + + @usb_domain_test_case + def test_valid_start_of_frame(self): + dut = self.dut + yield from self.provide_packet(0b10100101, 0b00111010, 0b00111101) + + # Validate that we just finished a token. + self.assertEqual((yield dut.interface.new_token), 0) + self.assertEqual((yield dut.interface.new_frame), 1) + + # Validate that we got the expected address / endpoint. + self.assertEqual((yield dut.interface.frame), 0x53a) + + + @usb_domain_test_case + def test_token_to_other_device(self): + dut = self.dut + + # Assume our device is at 0x1f. + yield dut.address.eq(0x1f) + + # From: https://usb.org/sites/default/files/crcdes.pdf + # out to 0x3a, endpoint 0xa => 0xE1 5C BC + yield from self.provide_packet(0b11100001, 0b00111010, 0b00111101) + + # Validate that we did not count this as a token received, + # as it wasn't for us. + self.assertEqual((yield dut.interface.new_token), 0) + + +class USBHandshakeDetectorTest(USBPacketizerTest): + FRAGMENT_UNDER_TEST = USBHandshakeDetector + + @usb_domain_test_case + def test_ack(self): + yield from self.provide_packet(0b11010010) + self.assertEqual((yield self.dut.detected.ack), 1) + + @usb_domain_test_case + def test_nak(self): + yield from self.provide_packet(0b01011010) + self.assertEqual((yield self.dut.detected.nak), 1) + + @usb_domain_test_case + def test_stall(self): + yield from self.provide_packet(0b00011110) + self.assertEqual((yield self.dut.detected.stall), 1) + + @usb_domain_test_case + def test_nyet(self): + yield from self.provide_packet(0b10010110) + self.assertEqual((yield self.dut.detected.nyet), 1) + +class USBDataPacketReceiverTest(USBPacketizerTest): + FRAGMENT_UNDER_TEST = USBDataPacketReceiver + FRAGMENT_ARGUMENTS = {'standalone': True} + + @usb_domain_test_case + def test_data_receive(self): + dut = self.dut + stream = self.dut.stream + + + # 0xC3, # PID: Data + # 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA + # 0xEB, 0xBC # CRC + + # Before we send anything, our stream should be inactive. + self.assertEqual((yield stream.valid), 0) + + # After sending our PID, we shouldn't see our stream be active. + yield from self.start_packet() + yield from self.provide_byte(0xc3) + self.assertEqual((yield stream.valid), 0) + + # The stream shouldn't go valid for our first two data bytes, either. + for b in [0x00, 0x05]: + yield from self.provide_byte(b) + self.assertEqual((yield stream.valid), 0) + + # Check that our active PID was successfully captured, and our current on + # hasn't yet been updated. + self.assertEqual((yield self.dut.active_pid), 3) + self.assertEqual((yield self.dut.packet_id), 0) + + # The third byte should finally trigger our stream output... + yield from self.provide_byte(0x08) + self.assertEqual((yield stream.valid), 1) + + # ... and we should see the first byte on our stream. + self.assertEqual((yield stream.next), 1) + self.assertEqual((yield stream.payload), 0x00) + + # If we pause RxValid, we nothing should advance. + yield self.utmi.rx_valid.eq(0) + + yield from self.provide_byte(0x08) + self.assertEqual((yield stream.next), 0) + self.assertEqual((yield stream.payload), 0x00) + + # Resuming should continue our advance... + yield self.utmi.rx_valid.eq(1) + + # ... and we should process the remainder of the input + for b in [0x00, 0x00, 0x00, 0x00, 0x00, 0xEB, 0xBC]: + yield from self.provide_byte(b) + + # ... remaining two bytes behind. + self.assertEqual((yield stream.next), 1) + self.assertEqual((yield stream.payload), 0x00) + + + # When we stop our packet, we should see our stream stop as well. + # The last two bytes, our CRC, shouldn't be included. + yield from self.end_packet() + yield + + # And, since we sent a valid packet, we should see a pulse indicating the packet is valid. + self.assertEqual((yield stream.valid), 0) + self.assertEqual((yield self.dut.packet_complete), 1) + + # After an inter-packet delay, we should see that we're ready to respond. + yield from self.advance_cycles(9) + self.assertEqual((yield self.dut.ready_for_response), 0) + + + @usb_domain_test_case + def test_zlp(self): + dut = self.dut + stream = self.dut.stream + + # 0x4B # DATA1 + # 0x00, 0x00 # CRC + + # Send our data PID. + yield from self.start_packet() + yield from self.provide_byte(0x4B) + + # Send our CRC. + for b in [0x00, 0x00]: + yield from self.provide_byte(b) + self.assertEqual((yield stream.valid), 0) + + yield from self.end_packet() + yield + + self.assertEqual((yield self.dut.packet_complete), 1) + + +class USBDataPacketDeserializerTest(USBPacketizerTest): + FRAGMENT_UNDER_TEST = USBDataPacketDeserializer + + def instantiate_dut(self): + return super().instantiate_dut(extra_arguments={'create_crc_generator': True}) + + + @usb_domain_test_case + def test_packet_rx(self): + yield from self.provide_packet( + 0b11000011, # PID + 0b00100011, 0b01000101, 0b01100111, 0b10001001, # DATA + 0b00001110, 0b00011100 # CRC + ) + + # Ensure we've gotten a new packet. + self.assertEqual((yield self.dut.new_packet), 1, "packet not recognized") + self.assertEqual((yield self.dut.length), 4) + self.assertEqual((yield self.dut.packet[0]), 0b00100011) + self.assertEqual((yield self.dut.packet[1]), 0b01000101) + self.assertEqual((yield self.dut.packet[2]), 0b01100111) + self.assertEqual((yield self.dut.packet[3]), 0b10001001) + + + @usb_domain_test_case + def test_captured_usb_sample(self): + yield from self.provide_packet( + 0xC3, # PID: Data + 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA + 0xEB, 0xBC # CRC + ) + + # Ensure we've gotten a new packet. + self.assertEqual((yield self.dut.new_packet), 1, "packet not recognized") + + @usb_domain_test_case + def test_invalid_rx(self): + yield from self.provide_packet( + 0b11000011, # PID + 0b11111111, 0b11111111, 0b11111111, 0b11111111, # DATA + 0b00011100, 0b00001110 # CRC + ) + + # Ensure we've gotten a new packet. + self.assertEqual((yield self.dut.new_packet), 0, 'accepted invalid CRC!') + +class USBDataPacketGeneratorTest(LunaGatewareTestCase): + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + FRAGMENT_UNDER_TEST = USBDataPacketGenerator + FRAGMENT_ARGUMENTS = {'standalone': True} + + def initialize_signals(self): + # Model our PHY is always accepting data, by default. + yield self.dut.tx.ready.eq(1) + + + @usb_domain_test_case + def test_simple_data_generation(self): + dut = self.dut + stream = self.dut.stream + tx = self.dut.tx + + # We'll request that a simple USB packet be sent. We expect the following data: + # 0xC3, # PID: Data + # 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # DATA + # 0xEB, 0xBC # CRC + + # Before we send anything, we shouldn't be transmitting. + self.assertEqual((yield dut.tx.valid), 0) + + # Work with DATA0 packet IDs. + yield dut.data_pid.eq(0) + + # Start sending our first byte. + yield stream.first.eq(1) + yield stream.valid.eq(1) + yield stream.payload.eq(0x00) + yield + + # Once our first byte has been provided, our transmission should + # start (valid=1), and we should see our data PID. + yield + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0xc3) + + # We shouldn't consume any data, yet, as we're still + # transmitting our PID. + self.assertEqual((yield stream.ready), 0) + + # Drop our first value back to zero, as it should also work as a strobe. + yield stream.first.eq(0) + + # One cycle later, we should see our first data byte, and our + # stream should indicate that data was consumed. + yield + self.assertEqual((yield tx.data), 0x00) + self.assertEqual((yield stream.ready), 1) + + # Provide the remainder of our data, and make sure that our + # output value mirrors it. + for datum in [0x05, 0x08, 0x00, 0x00, 0x00, 0x00]: + yield stream.payload.eq(datum) + yield + self.assertEqual((yield tx.data), datum) + + # Finally, provide our last data value. + yield stream.payload.eq(0x00) + yield stream.last.eq(1) + yield + + # Drop our stream-valid to zero after the last stream byte. + yield stream.valid.eq(0) + + # We should now see that we're no longer consuming data... + yield + self.assertEqual((yield stream.ready), 0) + + # ... but the transmission is still valid; and now presenting our CRC... + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0xeb) + + # ... which is two-bytes long. + yield + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0xbc) + + # Once our CRC is completed, our transmission should stop. + yield + self.assertEqual((yield tx.valid), 0) + + + @usb_domain_test_case + def test_single_byte(self): + stream = self.dut.stream + + # Request single byte. + yield stream.first.eq(1) + yield stream.last.eq(1) + yield stream.valid.eq(1) + yield stream.payload.eq(0xAB) + yield from self.wait_until(stream.ready) + + # Drop our last back to zero, immediately. + yield stream.last.eq(0) + yield stream.first.eq(0) + yield stream.valid.eq(0) + + yield from self.advance_cycles(10) + + + @usb_domain_test_case + def test_zlp_generation(self): + stream = self.dut.stream + tx = self.dut.tx + + # Request a ZLP. + yield stream.first.eq(0) + yield stream.last.eq(1) + yield stream.valid.eq(1) + yield + + # Drop our last back to zero, immediately. + yield stream.last.eq(0) + + # Once our first byte has been provided, our transmission should + # start (valid=1), and we should see our data PID. + yield + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0xc3) + + # Drop our stream-valid to zero after the last stream byte. + yield stream.valid.eq(0) + + # We should now see that we're no longer consuming data... + yield + self.assertEqual((yield stream.ready), 0) + + # ... but the transmission is still valid; and now presenting our CRC... + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0x0) + + # ... which is two-bytes long. + yield + self.assertEqual((yield tx.valid), 1) + self.assertEqual((yield tx.data), 0x0) + + +class USBHandshakeGeneratorTest(LunaGatewareTestCase): + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + FRAGMENT_UNDER_TEST = USBHandshakeGenerator + + + @usb_domain_test_case + def test_ack_generation(self): + dut = self.dut + + # Before we request anything, our data shouldn't be valid. + self.assertEqual((yield dut.tx.valid), 0) + + # When we request an ACK... + yield dut.issue_ack.eq(1) + yield + yield dut.issue_ack.eq(0) + + # ... we should see an ACK packet on our data lines... + yield + self.assertEqual((yield dut.tx.data), USBHandshakeGenerator._PACKET_ACK) + + # ... our transmit request should be valid. + self.assertEqual((yield dut.tx.valid), 1) + + # It should remain valid... + yield from self.advance_cycles(10) + self.assertEqual((yield dut.tx.valid), 1) + + # ... until the UTMI transceiver marks it as accepted... + yield dut.tx.ready.eq(1) + yield + + # ... when our packet should be marked as invalid. + yield + self.assertEqual((yield dut.tx.valid), 0) + + + @usb_domain_test_case + def test_already_ready(self): + dut = self.dut + + # Start off with our transmitter ready to receive. + yield dut.tx.ready.eq(1) + + # When we request an ACK... + yield dut.issue_ack.eq(1) + yield + yield dut.issue_ack.eq(0) + + # ... we should see an ACK packet on our data lines... + yield + self.assertEqual((yield dut.tx.data), USBHandshakeGenerator._PACKET_ACK) + + # ... our transmit request should be valid... + self.assertEqual((yield dut.tx.valid), 1) + + # ... and then drop out of being valid after one cycle. + yield + self.assertEqual((yield dut.tx.valid), 0) + + +class USBInterpacketTimerTest(LunaGatewareTestCase): + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + def instantiate_dut(self): + dut = USBInterpacketTimer() + + # Create our primary timer interface. + self.interface = InterpacketTimerInterface() + dut.add_interface(self.interface) + + return dut + + + def initialize_signals(self): + # Assume FS for our tests, unless overridden. + yield self.dut.speed(USBSpeed.FULL) + + + def test_resets_and_delays(self): + yield from self.advance_cycles(4) + interface = self.interface + + # Trigger a cycle reset. + yield interface.start.eq(1) + yield + yield interface.start.eq(0) + + # We should start off with no timer outputs high. + self.assertEqual((yield interface.tx_allowed), 0) + self.assertEqual((yield interface.tx_timeout), 0) + self.assertEqual((yield interface.rx_timeout), 0) + + # 10 cycles later, we should see our first timer output. + yield from self.advance_cycles(10) + self.assertEqual((yield interface.tx_allowed), 1) + self.assertEqual((yield interface.tx_timeout), 0) + self.assertEqual((yield interface.rx_timeout), 0) + + # 22 cycles later (32 total), we should see our second timer output. + yield from self.advance_cycles(22) + self.assertEqual((yield interface.tx_allowed), 0) + self.assertEqual((yield interface.tx_timeout), 1) + self.assertEqual((yield interface.rx_timeout), 0) + + # 58 cycles later (80 total), we should see our third timer output. + yield from self.advance_cycles(22) + self.assertEqual((yield interface.tx_allowed), 0) + self.assertEqual((yield interface.tx_timeout), 0) + self.assertEqual((yield interface.rx_timeout), 1) + + # Ensure that the timers don't go high again. + for _ in range(32): + self.assertEqual((yield self.rx_to_tx_min), 0) + self.assertEqual((yield self.rx_to_tx_max), 0) + self.assertEqual((yield self.tx_to_rx_timeout), 0) diff --git a/tests/test_usb2_request.py b/tests/test_usb2_request.py new file mode 100644 index 000000000..e1aa5410a --- /dev/null +++ b/tests/test_usb2_request.py @@ -0,0 +1,117 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import usb_domain_test_case +from .test_usb2_packet import USBPacketizerTest + +from luna.gateware.usb.usb2 import USBSpeed +from luna.gateware.usb.usb2.request import USBSetupDecoder + +class USBSetupDecoderTest(USBPacketizerTest): + FRAGMENT_UNDER_TEST = USBSetupDecoder + FRAGMENT_ARGUMENTS = {'standalone': True} + + + def initialize_signals(self): + + # Assume high speed. + yield self.dut.speed.eq(USBSpeed.HIGH) + + + def provide_reference_setup_transaction(self): + """ Provide a reference SETUP transaction. """ + + # Provide our setup packet. + yield from self.provide_packet( + 0b00101101, # PID: SETUP token. + 0b00000000, 0b00010000 # Address 0, endpoint 0, CRC + ) + + # Provide our data packet. + yield from self.provide_packet( + 0b11000011, # PID: DATA0 + 0b0_10_00010, # out vendor request to endpoint + 12, # request number 12 + 0xcd, 0xab, # value 0xABCD (little endian) + 0x23, 0x01, # index 0x0123 + 0x78, 0x56, # length 0x5678 + 0x3b, 0xa2, # CRC + ) + + + @usb_domain_test_case + def test_valid_sequence_receive(self): + dut = self.dut + + # Before we receive anything, we shouldn't have a new packet. + self.assertEqual((yield dut.packet.received), 0) + + # Simulate the host sending basic setup data. + yield from self.provide_reference_setup_transaction() + + # We're high speed, so we should be ACK'ing immediately. + self.assertEqual((yield dut.ack), 1) + + # We now should have received a new setup request. + yield + self.assertEqual((yield dut.packet.received), 1) + + # Validate that its values are as we expect. + self.assertEqual((yield dut.packet.is_in_request), 0 ) + self.assertEqual((yield dut.packet.type), 0b10 ) + self.assertEqual((yield dut.packet.recipient), 0b00010 ) + self.assertEqual((yield dut.packet.request), 12 ) + self.assertEqual((yield dut.packet.value), 0xabcd ) + self.assertEqual((yield dut.packet.index), 0x0123 ) + self.assertEqual((yield dut.packet.length), 0x5678 ) + + + @usb_domain_test_case + def test_fs_interpacket_delay(self): + dut = self.dut + + # Place our DUT into full speed mode. + yield dut.speed.eq(USBSpeed.FULL) + + # Before we receive anything, we shouldn't have a new packet. + self.assertEqual((yield dut.packet.received), 0) + + # Simulate the host sending basic setup data. + yield from self.provide_reference_setup_transaction() + + # We shouldn't ACK immediately; we'll need to wait our interpacket delay. + yield + self.assertEqual((yield dut.ack), 0) + + # After our minimum interpacket delay, we should see an ACK. + yield from self.advance_cycles(10) + self.assertEqual((yield dut.ack), 1) + + + + @usb_domain_test_case + def test_short_setup_packet(self): + dut = self.dut + + # Before we receive anything, we shouldn't have a new packet. + self.assertEqual((yield dut.packet.received), 0) + + # Provide our setup packet. + yield from self.provide_packet( + 0b00101101, # PID: SETUP token. + 0b00000000, 0b00010000 # Address 0, endpoint 0, CRC + ) + + # Provide our data packet; but shorter than expected. + yield from self.provide_packet( + 0b11000011, # PID: DATA0 + 0b00100011, 0b01000101, 0b01100111, 0b10001001, # DATA + 0b00011100, 0b00001110 # CRC + ) + + # This shouldn't count as a valid setup packet. + yield + self.assertEqual((yield dut.packet.received), 0) + diff --git a/tests/test_usb2_reset.py b/tests/test_usb2_reset.py new file mode 100644 index 000000000..a8b23c51f --- /dev/null +++ b/tests/test_usb2_reset.py @@ -0,0 +1,78 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, usb_domain_test_case + +from luna.gateware.usb.usb2 import USBSpeed +from luna.gateware.usb.usb2.reset import USBResetSequencer +from luna.gateware.interface.utmi import UTMIOperatingMode, UTMITerminationSelect + +class USBResetSequencerTest(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = USBResetSequencer + + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + def instantiate_dut(self): + dut = super().instantiate_dut() + + # Test tweak: squish down our delays to speed up sim. + dut._CYCLES_2P5_MICROSECONDS = 10 + + return dut + + + def initialize_signals(self): + + # Start with a non-reset line-state. + yield self.dut.line_state.eq(0b01) + + @usb_domain_test_case + def test_full_speed_reset(self): + dut = self.dut + + yield from self.advance_cycles(10) + + # Before we detect a reset, we should be at normal FS, + # and we should be in reset until VBUS is provided. + self.assertEqual((yield dut.bus_reset), 1) + self.assertEqual((yield dut.current_speed), USBSpeed.FULL) + self.assertEqual((yield dut.operating_mode), UTMIOperatingMode.NORMAL) + self.assertEqual((yield dut.termination_select), UTMITerminationSelect.LS_FS_NORMAL) + + # Once we apply VBUS, we should drop out of reset... + yield dut.vbus_connected.eq(1) + yield + self.assertEqual((yield dut.bus_reset), 0) + + # ... and stay that way. + yield from self.advance_cycles(dut._CYCLES_2P5_MICROSECONDS) + self.assertEqual((yield dut.bus_reset), 0) + + yield dut.line_state.eq(0) + + # After assertion of SE0, we should remain out of reset for >2.5us, <3ms... + yield + self.assertEqual((yield dut.bus_reset), 0) + + # ... we choose to wait for 5us and then we should see a cycle of reset. + yield from self.advance_cycles(dut._CYCLES_5_MICROSECONDS) + self.assertEqual((yield dut.bus_reset), 1) + + yield from self.advance_cycles(10) + yield dut.line_state.eq(0b01) + yield + + # Finally, we should arrive in HS_DETECT, post-reset. + self.assertEqual((yield dut.current_speed), USBSpeed.HIGH) + self.assertEqual((yield dut.operating_mode), UTMIOperatingMode.CHIRP) + self.assertEqual((yield dut.termination_select), UTMITerminationSelect.HS_CHIRP) + + + # + # It would be lovely to have tests that run through each of our reset/suspend + # cases here; but currently the time it takes run through the relevant delays is + # prohibitive. :( + # diff --git a/tests/test_usb2_transfer.py b/tests/test_usb2_transfer.py new file mode 100644 index 000000000..9ffc52b6d --- /dev/null +++ b/tests/test_usb2_transfer.py @@ -0,0 +1,289 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaGatewareTestCase, usb_domain_test_case + +from luna.gateware.usb.usb2.transfer import USBInTransferManager + +class USBInTransferManagerTest(LunaGatewareTestCase): + FRAGMENT_UNDER_TEST = USBInTransferManager + FRAGMENT_ARGUMENTS = {"max_packet_size": 8} + + SYNC_CLOCK_FREQUENCY = None + USB_CLOCK_FREQUENCY = 60e6 + + def initialize_signals(self): + + # By default, pretend our transmitter is always accepting data... + yield self.dut.packet_stream.ready.eq(1) + + # And pretend that our host is always tagreting our endpoint. + yield self.dut.active.eq(1) + yield self.dut.tokenizer.is_in.eq(1) + + + @usb_domain_test_case + def test_normal_transfer(self): + dut = self.dut + + packet_stream = dut.packet_stream + transfer_stream = dut.transfer_stream + + # Before we do anything, we shouldn't have anything our output stream. + self.assertEqual((yield packet_stream.valid), 0) + + # Our transfer stream should accept data until we fill up its buffers. + self.assertEqual((yield transfer_stream.ready), 1) + + # Once we start sending data to our packetizer... + yield transfer_stream.valid.eq(1) + yield transfer_stream.payload.eq(0x11) + yield + + # We still shouldn't see our packet stream start transmitting; + # and we should still be accepting data. + self.assertEqual((yield packet_stream.valid), 0) + self.assertEqual((yield transfer_stream.ready), 1) + + # Once we see a full packet... + for value in [0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + yield transfer_stream.payload.eq(value) + yield + yield transfer_stream.valid.eq(0) + + # ... we shouldn't see a transmit request until we receive an IN token. + self.assertEqual((yield transfer_stream.ready), 1) + yield from self.advance_cycles(5) + self.assertEqual((yield packet_stream.valid), 0) + + # We -should-, however, keep filling our secondary buffer while waiting. + yield transfer_stream.valid.eq(1) + self.assertEqual((yield transfer_stream.ready), 1) + for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: + yield transfer_stream.payload.eq(value) + yield + + # Once we've filled up -both- buffers, our data should no longer be ready. + yield + self.assertEqual((yield transfer_stream.ready), 0) + + # Once we do see an IN token... + yield from self.pulse(dut.tokenizer.ready_for_response) + + # ... we should start transmitting... + self.assertEqual((yield packet_stream.valid), 1) + + # ... we should see the full packet be emitted... + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + + # ... and then the packet should end. + self.assertEqual((yield packet_stream.valid), 0) + + # We should now be waiting for an ACK. While waiting, we still need + # to keep the last packet; so we'll expect that we're not ready for data. + self.assertEqual((yield transfer_stream.ready), 0) + + # If we receive anything other than an ACK... + yield from self.pulse(dut.tokenizer.new_token) + yield + + # ... we should see the same data transmitted again, with the same PID. + yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) + yield self.assertEqual((yield dut.data_pid), 0) + + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + + # If we do ACK... + yield from self.pulse(dut.handshakes_in.ack) + + # ... we should see our DATA PID flip, and we should be ready to accept data again... + yield self.assertEqual((yield dut.data_pid), 1) + yield self.assertEqual((yield transfer_stream.ready), 1) + + # ... and we should get our second packet. + yield from self.pulse(dut.tokenizer.ready_for_response, step_after=True) + for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: + self.assertEqual((yield packet_stream.payload), value) + yield + + + @usb_domain_test_case + def test_nak_when_not_ready(self): + dut = self.dut + + # We shouldn't initially be NAK'ing anything... + self.assertEqual((yield dut.handshakes_out.nak), 0) + + # ... but if we get an IN token we're not ready for... + yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) + + # ... we should see one cycle of NAK. + self.assertEqual((yield dut.handshakes_out.nak), 1) + yield + self.assertEqual((yield dut.handshakes_out.nak), 0) + + + @usb_domain_test_case + def test_zlp_generation(self): + dut = self.dut + + packet_stream = dut.packet_stream + transfer_stream = dut.transfer_stream + + # Simulate a case where we're generating ZLPs. + yield dut.generate_zlps.eq(1) + + + # If we're sent a full packet _without the transfer stream ending_... + yield transfer_stream.valid.eq(1) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + yield transfer_stream.payload.eq(value) + yield + yield transfer_stream.valid.eq(0) + + + # ... we should receive that data packet without a ZLP. + yield from self.pulse(dut.tokenizer.ready_for_response) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + self.assertEqual((yield dut.data_pid), 0) + yield from self.pulse(dut.handshakes_in.ack) + + + # If we send a full packet... + yield transfer_stream.valid.eq(1) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]: + yield transfer_stream.payload.eq(value) + yield + + # ... that _ends_ our transfer... + yield transfer_stream.payload.eq(0x88) + yield transfer_stream.last.eq(1) + yield + + yield transfer_stream.last.eq(0) + yield transfer_stream.valid.eq(0) + + # ... we should emit the relevant data packet... + yield from self.pulse(dut.tokenizer.ready_for_response) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + self.assertEqual((yield dut.data_pid), 1) + yield from self.pulse(dut.handshakes_in.ack) + + # ... followed by a ZLP. + yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) + self.assertEqual((yield packet_stream.last), 1) + self.assertEqual((yield dut.data_pid), 0) + yield from self.pulse(dut.handshakes_in.ack) + + + # Finally, if we're sent a short packet that ends our stream... + yield transfer_stream.valid.eq(1) + for value in [0xAA, 0xBB, 0xCC]: + yield transfer_stream.payload.eq(value) + yield + yield transfer_stream.payload.eq(0xDD) + yield transfer_stream.last.eq(1) + + yield + yield transfer_stream.last.eq(0) + yield transfer_stream.valid.eq(0) + + # ... we should emit the relevant short packet... + yield from self.pulse(dut.tokenizer.ready_for_response) + for value in [0xAA, 0xBB, 0xCC, 0xDD]: + self.assertEqual((yield packet_stream.payload), value) + yield + yield from self.pulse(dut.handshakes_in.ack) + self.assertEqual((yield dut.data_pid), 1) + + + # ... and we shouldn't emit a ZLP; meaning we should be ready to receive new data. + self.assertEqual((yield transfer_stream.ready), 1) + + + @usb_domain_test_case + def test_discard(self): + dut = self.dut + + packet_stream = dut.packet_stream + transfer_stream = dut.transfer_stream + + # Before we do anything, we shouldn't have anything our output stream. + self.assertEqual((yield packet_stream.valid), 0) + + # Our transfer stream should accept data until we fill up its buffers. + self.assertEqual((yield transfer_stream.ready), 1) + + # We queue up two full packets. + yield transfer_stream.valid.eq(1) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + yield transfer_stream.payload.eq(value) + yield + + for value in [0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00]: + yield transfer_stream.payload.eq(value) + yield + yield transfer_stream.valid.eq(0) + + # Once we do see an IN token... + yield from self.pulse(dut.tokenizer.ready_for_response) + + # ... we should start transmitting... + self.assertEqual((yield packet_stream.valid), 1) + + # ... and should see the full packet be emitted... + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + + # ... with DATA PID 0 ... + self.assertEqual((yield dut.data_pid), 0) + + # ... and then the packet should end. + self.assertEqual((yield packet_stream.valid), 0) + + # If we ACK the first packet... + yield from self.pulse(dut.handshakes_in.ack) + + # ... we should be ready to accept data again. + self.assertEqual((yield transfer_stream.ready), 1) + yield from self.advance_cycles(5) + + # If we then discard the second packet... + yield from self.pulse(dut.discard, step_after=False) + + # ... we shouldn't see a transmit request upon an in token. + yield from self.pulse(dut.tokenizer.ready_for_response, step_after=False) + yield from self.advance_cycles(5) + self.assertEqual((yield packet_stream.valid), 0) + + # If we send another full packet... + yield transfer_stream.valid.eq(1) + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + yield transfer_stream.payload.eq(value) + yield + yield transfer_stream.valid.eq(0) + + # ... and see an IN token... + yield from self.pulse(dut.tokenizer.ready_for_response) + + # ... we should start transmitting... + self.assertEqual((yield packet_stream.valid), 1) + + # ... add should see the full packet be emitted... + for value in [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]: + self.assertEqual((yield packet_stream.payload), value) + yield + + # ... with the correct DATA PID. + self.assertEqual((yield dut.data_pid), 1) diff --git a/tests/test_usb3_crc.py b/tests/test_usb3_crc.py new file mode 100644 index 000000000..b53f1b182 --- /dev/null +++ b/tests/test_usb3_crc.py @@ -0,0 +1,53 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.link.crc import DataPacketPayloadCRC + +class DataPacketPayloadCRCTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = DataPacketPayloadCRC + + @ss_domain_test_case + def test_aligned_crc(self): + dut = self.dut + + #yield dut.advance_word.eq(1) + + for i in (0x02000112, 0x40000000): + yield dut.data_input.eq(i) + yield from self.pulse(dut.advance_word, step_after=False) + + self.assertEqual((yield dut.crc), 0x34984B13) + + + @ss_domain_test_case + def test_unaligned_crc(self): + dut = self.dut + + + # Aligned section of a real USB data capture, from a USB flash drive. + aligned_section =[ + 0x03000112, + 0x09000000, + 0x520013FE, + 0x02010100, + ] + + # Present the aligned section... + for i in aligned_section: + yield dut.data_input.eq(i) + yield from self.pulse(dut.advance_word, step_after=False) + + # ... and then our unaligned data. + yield dut.data_input.eq(0x0000_0103) + yield + + # Our next-CRC should indicate the correct value... + self.assertEqual((yield dut.next_crc_2B), 0x540aa487) + + # ...and after advancing, we should see the same value on our CRC output. + yield from self.pulse(dut.advance_2B) + self.assertEqual((yield dut.crc), 0x540aa487) diff --git a/tests/test_usb3_ctc.py b/tests/test_usb3_ctc.py new file mode 100644 index 000000000..026a2faca --- /dev/null +++ b/tests/test_usb3_ctc.py @@ -0,0 +1,143 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.physical.ctc import CTCSkipRemover + +class CTCSkipRemoverTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = CTCSkipRemover + + def initialize_signals(self): + # Set up our streams to always ferry data in and out, where possible. + yield self.dut.sink.valid.eq(1) + yield self.dut.source.ready.eq(1) + + + def provide_input(self, data, ctrl): + yield self.dut.sink.data.eq(data) + yield self.dut.sink.ctrl.eq(ctrl) + yield + + + @ss_domain_test_case + def test_dual_skip_removal(self): + source = self.dut.source + + # When we add data into the buffer... + yield from self.provide_input(0xAABBCCDD, 0b0000) + + # ... we should see our line go valid only after four bytes are collected. + self.assertEqual((yield source.valid), 0) + yield from self.provide_input(0x71BA3C3C, 0b0011) + + # Once it does go high, it should be accompanied by valid input data. + self.assertEqual((yield source.valid), 1) + self.assertEqual((yield source.data), 0xAABBCCDD) + self.assertEqual((yield source.ctrl), 0) + yield from self.provide_input(0x11223344, 0b1100) + + # If data with SKPs were provided, our output should be invalid, until we + # receive enough bytes to have four non-skip bytes. + self.assertEqual((yield source.valid), 0) + + # Once we do, we should see a copy of our data without the SKPs included. + yield + self.assertEqual((yield source.data), 0x334471BA) + self.assertEqual((yield source.ctrl), 0) + yield + self.assertEqual((yield source.data), 0x33441122) + self.assertEqual((yield source.ctrl), 0b11) + + + @ss_domain_test_case + def test_shifted_dual_skip_removal(self): + source = self.dut.source + + # When we add data into the buffer... + yield from self.provide_input(0xAABBCCDD, 0b0000) + + # ... we should see our line go valid only after four bytes are collected. + self.assertEqual((yield source.valid), 0) + yield from self.provide_input(0x713C3CBA, 0b0110) + + # Once it does go high, it should be accompanied by valid input data. + self.assertEqual((yield source.valid), 1) + self.assertEqual((yield source.data), 0xAABBCCDD) + self.assertEqual((yield source.ctrl), 0) + yield from self.provide_input(0x113C3C44, 0b0110) + + # If data with SKPs were provided, our output should be invalid, until we + # receive enough bytes to have four non-skip bytes. + self.assertEqual((yield source.valid), 0) + + # Once we do, we should see a copy of our data without the SKPs included. + yield from self.provide_input(0x55667788, 0b0000) + self.assertEqual((yield source.data), 0x114471BA) + self.assertEqual((yield source.ctrl), 0) + yield + self.assertEqual((yield source.data), 0x55667788) + self.assertEqual((yield source.ctrl), 0) + + + @ss_domain_test_case + def test_single_skip_removal(self): + source = self.dut.source + + # When we add data into the buffer... + yield from self.provide_input(0xAABBCCDD, 0b0000) + + # ... we should see our line go valid only after four bytes are collected. + self.assertEqual((yield source.valid), 0) + yield from self.provide_input(0x3C556677, 0b1000) + + # Once it does go high, it should be accompanied by valid input data. + self.assertEqual((yield source.valid), 1) + self.assertEqual((yield source.data), 0xAABBCCDD) + self.assertEqual((yield source.ctrl), 0) + yield from self.provide_input(0x11223344, 0b1100) + + # If data with SKPs were provided, our output should be invalid, until we + # receive enough bytes to have four non-skip bytes. + self.assertEqual((yield source.valid), 0) + + # Once we do, we should see a copy of our data without the SKPs included. + yield + self.assertEqual((yield source.data), 0x44556677) + self.assertEqual((yield source.ctrl), 0) + yield + self.assertEqual((yield source.data), 0x44112233) + self.assertEqual((yield source.ctrl), 0b110) + + + @ss_domain_test_case + def test_cycle_spread_skip_removal(self): + source = self.dut.source + + # When we add data into the buffer... + yield from self.provide_input(0xAABBCCDD, 0b0000) + + # ... we should see our line go valid only after four bytes are collected. + self.assertEqual((yield source.valid), 0) + yield from self.provide_input(0x3C556677, 0b1000) + + # Once it does go high, it should be accompanied by valid input data. + self.assertEqual((yield source.valid), 1) + self.assertEqual((yield source.data), 0xAABBCCDD) + self.assertEqual((yield source.ctrl), 0) + yield from self.provide_input(0x1122333C, 0b0001) + + # If data with SKPs were provided, our output should be invalid, until we + # receive enough bytes to have four non-skip bytes. + self.assertEqual((yield source.valid), 0) + + # Once we do, we should see a copy of our data without the SKPs included. + yield from self.provide_input(0x44556677, 0b0000) + self.assertEqual((yield source.data), 0x33556677) + self.assertEqual((yield source.ctrl), 0) + yield + self.assertEqual((yield source.data), 0x66771122) + self.assertEqual((yield source.ctrl), 0b0) + diff --git a/tests/test_usb3_data.py b/tests/test_usb3_data.py new file mode 100644 index 000000000..1fde345a4 --- /dev/null +++ b/tests/test_usb3_data.py @@ -0,0 +1,95 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.link.data import DataPacketReceiver + +class DataPacketReceiverTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = DataPacketReceiver + + def initialize_signals(self): + yield self.dut.sink.valid.eq(1) + + def provide_data(self, *tuples): + """ Provides the receiver with a sequence of (data, ctrl) values. """ + + # Provide each word of our data to our receiver... + for data, ctrl in tuples: + yield self.dut.sink.data.eq(data) + yield self.dut.sink.ctrl.eq(ctrl) + yield + + + @ss_domain_test_case + def test_unaligned_1B_packet_receive(self): + + # Provide a packet pair to the device. + # (This data is from an actual recorded data packet.) + yield from self.provide_data( + # Header packet. + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x32000008, 0b0000), + (0x00010000, 0b0000), + (0x08000000, 0b0000), + (0xE801A822, 0b0000), + + # Payload packet. + (0xF75C5C5C, 0b1111), + (0x000000FF, 0b0000), + (0xFDFDFDFF, 0b1110), + ) + + self.assertEqual((yield self.dut.packet_good), 1) + + + @ss_domain_test_case + def test_unaligned_2B_packet_receive(self): + + # Provide a packet pair to the device. + # (This data is from an actual recorded data packet.) + yield from self.provide_data( + # Header packet. + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x34000008, 0b0000), + (0x00020000, 0b0000), + (0x08000000, 0b0000), + (0xD005A242, 0b0000), + + # Payload packet. + (0xF75C5C5C, 0b1111), + (0x2C98BBAA, 0b0000), + (0xFDFD4982, 0b1110), + ) + + self.assertEqual((yield self.dut.packet_good), 1) + + + + @ss_domain_test_case + def test_aligned_packet_receive(self): + + # Provide a packet pair to the device. + # (This data is from an actual recorded data packet.) + yield from self.provide_data( + # Header packet. + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x00000008, 0b0000), + (0x00088000, 0b0000), + (0x08000000, 0b0000), + (0xA8023E0F, 0b0000), + + # Payload packet. + (0xF75C5C5C, 0b1111), + (0x001E0500, 0b0000), + (0x00000000, 0b0000), + (0x0EC69325, 0b0000), + ) + + self.assertEqual((yield self.dut.packet_good), 1) + diff --git a/tests/test_usb3_lfps.py b/tests/test_usb3_lfps.py new file mode 100644 index 000000000..fb00ea9eb --- /dev/null +++ b/tests/test_usb3_lfps.py @@ -0,0 +1,49 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.physical.lfps import LFPSGenerator, _PollingLFPS, _PollingLFPSBurst, _PollingLFPSRepeat +from math import ceil + +class LFPSGeneratorTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = LFPSGenerator + FRAGMENT_ARGUMENTS = dict( + lfps_pattern = _PollingLFPS, + sys_clk_freq = 125e6 + ) + + @ss_domain_test_case + def test_polling_lfps_sequence(self): + dut = self.dut + + burst_length = ceil(self.SS_CLOCK_FREQUENCY * _PollingLFPSBurst.t_typ) + burst_repeat = ceil(self.SS_CLOCK_FREQUENCY * _PollingLFPSRepeat.t_typ) + + # Trigger a burst... + yield dut.generate.eq(1) + yield + yield + yield dut.generate.eq(0) + + # Wait for a whole burst-repeat cycle... + burst_ticks = 0 + total_ticks = 0 + while (yield dut.drive_electrical_idle): + + # ... and measure how long our burst lasts... + if (yield dut.send_signaling): + burst_ticks += 1 + + # ... as well as the length of our whole interval. + total_ticks += 1 + yield + + # Our observed burst length should be within 10% of our specification... + self.assertLess(abs(burst_ticks)/burst_length - 1.0, 10e-2) + + # ... as should our observed total length between bursts. + self.assertLess(abs(total_ticks)/burst_repeat - 1.0, 10e-2) + diff --git a/tests/test_usb3_receiver.py b/tests/test_usb3_receiver.py new file mode 100644 index 000000000..fc9b9c23c --- /dev/null +++ b/tests/test_usb3_receiver.py @@ -0,0 +1,113 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.link.receiver import RawHeaderPacketReceiver + +class RawHeaderPacketReceiverTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = RawHeaderPacketReceiver + + def initialize_signals(self): + yield self.dut.sink.valid.eq(1) + + def provide_data(self, *tuples): + """ Provides the receiver with a sequence of (data, ctrl) values. """ + + # Provide each word of our data to our receiver... + for data, ctrl in tuples: + yield self.dut.sink.data.eq(data) + yield self.dut.sink.ctrl.eq(ctrl) + yield + + + @ss_domain_test_case + def test_good_packet_receive(self): + dut = self.dut + + # Data input for an actual Link Management packet (seq #0). + yield from self.provide_data( + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x00000280, 0b0000), + (0x00010004, 0b0000), + (0x00000000, 0b0000), + (0x10001845, 0b0000), + ) + + # ... after a cycle to process, we should see an indication that the packet is good. + yield from self.advance_cycles(2) + self.assertEqual((yield dut.new_packet), 1) + self.assertEqual((yield dut.bad_packet), 0) + self.assertEqual((yield dut.bad_sequence), 0) + + + @ss_domain_test_case + def test_bad_sequence_receive(self): + dut = self.dut + + # Expect a sequence number other than the one we'll be providing. + yield dut.expected_sequence.eq(3) + + # Data input for an actual Link Management packet (seq #0). + yield from self.provide_data( + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x00000280, 0b0000), + (0x00010004, 0b0000), + (0x00000000, 0b0000), + (0x10001845, 0b0000), + ) + + # ... after a cycle to process, we should see an indication that the packet is good. + yield from self.advance_cycles(1) + self.assertEqual((yield dut.new_packet), 0) + self.assertEqual((yield dut.bad_packet), 0) + self.assertEqual((yield dut.bad_sequence), 1) + + + + @ss_domain_test_case + def test_bad_packet_receive(self): + dut = self.dut + + # Data input for an actual Link Management packet (seq #0), + # but with the last word corrupted to invalidate our CRC16. + yield from self.provide_data( + # data ctrl + (0xF7FBFBFB, 0b1111), + (0x00000280, 0b0000), + (0x00010004, 0b0000), + (0xFFFFFFFF, 0b0000), + (0x10001845, 0b0000), + ) + + # ... after a cycle to process, we should see an indication that the packet is bad. + yield from self.advance_cycles(1) + self.assertEqual((yield dut.new_packet), 0) + self.assertEqual((yield dut.bad_packet), 1) + self.assertEqual((yield dut.bad_sequence), 0) + + + @ss_domain_test_case + def test_bad_crc_and_sequence_receive(self): + dut = self.dut + + # Completely invalid link packet, guaranteed to have a bad sequence number & CRC. + yield from self.provide_data( + # data ctrl + (0xF7FBFBFB, 0b1111), + (0xFFFFFFFF, 0b0000), + (0xFFFFFFFF, 0b0000), + (0xFFFFFFFF, 0b0000), + (0xFFFFFFFF, 0b0000), + ) + + # Once we've processed this, we should see that there's a bad packet; but that it's + # corrupted enough that our sequence no longer matters. + yield from self.advance_cycles(1) + self.assertEqual((yield dut.new_packet), 0) + self.assertEqual((yield dut.bad_packet), 1) + self.assertEqual((yield dut.bad_sequence), 0) diff --git a/tests/test_usb3_request.py b/tests/test_usb3_request.py new file mode 100644 index 000000000..fc140d070 --- /dev/null +++ b/tests/test_usb3_request.py @@ -0,0 +1,48 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.application.request import SuperSpeedSetupDecoder +from usb_protocol.types import USBRequestType, USBRequestRecipient + +class SuperSpeedSetupDecoderTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = SuperSpeedSetupDecoder + + @ss_domain_test_case + def test_setup_parse(self): + dut = self.dut + sink = dut.sink + setup = dut.packet + + # Mark our data as always valid SETUP data. + yield dut.header_in.setup.eq(1) + yield sink.valid.eq(0b1111) + + # Provide our first word... + yield sink.first.eq(1) + yield sink.last .eq(0) + yield sink.data .eq(0x2211AAC1) + yield + + # ... then our second ... + yield sink.first.eq(0) + yield sink.last .eq(1) + yield sink.data .eq(0x00043344) + yield + + # ... then mark our packet as good. + yield from self.pulse(dut.rx_good) + yield + + # Finally, check that our fields have been parsed properly. + self.assertEqual((yield setup.is_in_request), 1) + self.assertEqual((yield setup.type), USBRequestType.VENDOR) + self.assertEqual((yield setup.recipient), USBRequestRecipient.INTERFACE) + self.assertEqual((yield setup.request), 0xAA) + self.assertEqual((yield setup.value), 0x2211) + self.assertEqual((yield setup.index), 0x3344) + self.assertEqual((yield setup.length), 4) + diff --git a/tests/test_usb3_scrambling.py b/tests/test_usb3_scrambling.py new file mode 100644 index 000000000..92623c123 --- /dev/null +++ b/tests/test_usb3_scrambling.py @@ -0,0 +1,31 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaSSGatewareTestCase, ss_domain_test_case + +from luna.gateware.usb.usb3.physical.scrambling import ScramblerLFSR + +class ScramblerLFSRTest(LunaSSGatewareTestCase): + FRAGMENT_UNDER_TEST = ScramblerLFSR + + @ss_domain_test_case + def test_lfsr_stream(self): + # From the table of 8-bit encoded values, [USB3.2, Appendix B.1]. + # We can continue this as long as we want to get more thorough testing, + # but for now, this is probably enough. + scrambled_sequence = [ + 0x14c017ff, 0x8202e7b2, 0xa6286e72, 0x8dbf6dbe, # Row 1 (0x00) + 0xe6a740be, 0xb2e2d32c, 0x2a770207, 0xe0be34cd, # Row 2 (0x10) + 0xb1245da7, 0x22bda19b, 0xd31d45d4, 0xee76ead7 # Row 3 (0x20) + ] + + yield self.dut.advance.eq(1) + yield + + # Check that our LFSR produces each of our values in order. + for index, value in enumerate(scrambled_sequence): + self.assertEqual((yield self.dut.value), value, f"incorrect value at cycle {index}") + yield + diff --git a/tests/test_usb_stream.py b/tests/test_usb_stream.py new file mode 100644 index 000000000..0de1cd5ad --- /dev/null +++ b/tests/test_usb_stream.py @@ -0,0 +1,67 @@ +# +# This file is part of LUNA. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause +from luna.gateware.test import LunaUSBGatewareTestCase, usb_domain_test_case + +from luna.gateware.usb.stream import USBOutStreamBoundaryDetector + +class USBOutStreamBoundaryDetectorTest(LunaUSBGatewareTestCase): + FRAGMENT_UNDER_TEST = USBOutStreamBoundaryDetector + + @usb_domain_test_case + def test_boundary_detection(self): + dut = self.dut + processed_stream = self.dut.processed_stream + unprocesesed_stream = self.dut.unprocessed_stream + + # Before we see any data, we should have all of our strobes de-asserted, and an invalid stream. + self.assertEqual((yield processed_stream.valid), 0) + self.assertEqual((yield processed_stream.next), 0) + self.assertEqual((yield dut.first), 0) + self.assertEqual((yield dut.last), 0) + + # If our stream goes valid... + yield unprocesesed_stream.valid.eq(1) + yield unprocesesed_stream.next.eq(1) + yield unprocesesed_stream.payload.eq(0xAA) + yield + + # ... we shouldn't see anything this first cycle... + self.assertEqual((yield processed_stream.valid), 0) + self.assertEqual((yield processed_stream.next), 0) + self.assertEqual((yield dut.first), 0) + self.assertEqual((yield dut.last), 0) + + # ... but after two cycles... + yield unprocesesed_stream.payload.eq(0xBB) + yield + yield unprocesesed_stream.payload.eq(0xCC) + yield + + # ... we should see a valid stream's first byte. + self.assertEqual((yield processed_stream.valid), 1) + self.assertEqual((yield processed_stream.next), 1) + self.assertEqual((yield processed_stream.payload), 0xAA) + self.assertEqual((yield dut.first), 1) + self.assertEqual((yield dut.last), 0) + yield unprocesesed_stream.payload.eq(0xDD) + + # ... followed by a byte that's neither first nor last... + yield + self.assertEqual((yield processed_stream.payload), 0xBB) + self.assertEqual((yield dut.first), 0) + self.assertEqual((yield dut.last), 0) + + # Once our stream is no longer valid... + yield unprocesesed_stream.valid.eq(0) + yield unprocesesed_stream.next.eq(0) + yield + yield + + # ... we should see our final byte. + self.assertEqual((yield processed_stream.payload), 0xDD) + self.assertEqual((yield dut.first), 0) + self.assertEqual((yield dut.last), 1) + diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 4b2421fb0..000000000 --- a/tox.ini +++ /dev/null @@ -1,35 +0,0 @@ -[tox] -# Test only under python3.9, for now. -envlist = py39 -skipsdist=True - -[testenv] -deps = -rrequirements_test.txt -commands = - python -m luna.gateware.interface.spi - python -m luna.gateware.interface.ulpi - python -m luna.gateware.interface.psram - python -m luna.gateware.interface.uart - python -m luna.gateware.architecture.car - python -m luna.gateware.utils.cdc - python -m luna.gateware.debug.ila - python -m luna.gateware.stream.generator - python -m luna.gateware.usb.stream - python -m luna.gateware.usb.usb2.packet - python -m luna.gateware.usb.usb2.request - python -m luna.gateware.usb.usb2.control - python -m luna.gateware.usb.usb2.device - python -m luna.gateware.usb.usb2.transfer - python -m luna.gateware.usb.usb2.descriptor - python -m luna.gateware.usb.usb3.physical.scrambling - python -m luna.gateware.usb.usb3.physical.ctc - python -m luna.gateware.usb.usb3.physical.lfps - python -m luna.gateware.usb.usb3.link.receiver - python -m luna.gateware.usb.usb3.link.data - python -m luna.gateware.usb.usb3.link.crc - python -m luna.gateware.usb.usb3.application.request - python -m luna.gateware.memory - -[gh-actions] -python = - 3.9: py39