diff --git a/doc/uart_interface.md b/doc/uart_interface.md index eeb431d..ba40b5c 100644 --- a/doc/uart_interface.md +++ b/doc/uart_interface.md @@ -15,6 +15,7 @@ uart: port: "auto" baudrate: 3000000 clock_freq: 100000000 + stall_interval: 16 chunk_size: 256 ``` Inside this configuration, the following parameters may be set: @@ -25,7 +26,9 @@ Inside this configuration, the following parameters may be set: - `clock_freq` _(required)_: The frequency of the clock provided to the `manta` module, in Hertz (Hz). This is used to calculate an appropriate prescaler onboard the FPGA to acheive the desired baudrate. Manta will throw an error if this clock frequency does not allow you to achieve your desired baudrate. -- `chunk_size` _(optional)_: The number of read requests to send at a time. Since the FPGA responds to read requests almost instantly, sending them in batches prevents the host machine's input buffer from overflowing. Defaults to 256, reduce this if Manta reports that bytes are being dropped. +- `stall_interval` _(optional)_: The number of read requests to send before sending a stall byte. This prevents packets from being dropped if the FPGA's baudrate is less than the USB-Serial adapter's baudrate. This is usually caused by a mismatch between the clock frequency of the USB-Serial adapter and the FPGA fabric. See issue [#18](https://github.com/fischermoseley/manta/issues/18) on GitHub. Defaults to 16, reduce this if Manta reports that bytes are being dropped. + +- `chunk_size` _(optional)_: The number of read requests to send at a time. Since the FPGA responds to read requests almost instantly, sending them in batches prevents the host machine's input buffer from overflowing. Defaults to 256, Reduce this if Manta reports that bytes are being dropped, and decreasing `stall_interval` did not work. ### Amaranth-Native Designs diff --git a/src/manta/uart/__init__.py b/src/manta/uart/__init__.py index 42a0ab9..ebacc8e 100644 --- a/src/manta/uart/__init__.py +++ b/src/manta/uart/__init__.py @@ -14,7 +14,7 @@ class UARTInterface(Elaboratable): the FPGA. """ - def __init__(self, port, baudrate, clock_freq, chunk_size=256): + def __init__(self, port, baudrate, clock_freq, stall_interval=16, chunk_size=256): """ This function is the main mechanism for configuring a UART Interface in an Amaranth-native design. @@ -37,11 +37,20 @@ class UARTInterface(Elaboratable): appropriate prescaler onboard the FPGA to acheive the desired baudrate. + stall_interval (Optional[int]): The number of read requests to send + before sending a stall byte. This prevents packets from being + dropped if the FPGA's baudrate is less than the USB-Serial + adapter's baudrate. This is usually caused by a mismatch + between the clock frequency of the USB-Serial adapter and the + FPGA fabric. See issue #18 on GitHub. Reduce this if Manta + reports that bytes are being dropped. + chunk_size (Optional[int]): The number of read requests to send at a time. Since the FPGA responds to read requests almost instantly, sending them in batches prevents the host machine's input buffer from overflowing. Reduce this if Manta reports - that bytes are being dropped. + that bytes are being dropped, and decreasing `stall_interval` + did not work. Raises: ValueError: The baudrate is not acheivable with the clock frequency @@ -52,8 +61,9 @@ class UARTInterface(Elaboratable): self._port = port self._baudrate = baudrate self._clock_freq = clock_freq - self._chunk_size = chunk_size self._clocks_per_baud = int(self._clock_freq // self._baudrate) + self._chunk_size = chunk_size + self._stall_interval = stall_interval self._check_config() # Top-Level Ports @@ -70,24 +80,27 @@ class UARTInterface(Elaboratable): baudrate = config.get("baudrate") # Warn if unrecognized options have been given - recognized_options = ["port", "clock_freq", "baudrate", "chunk_size"] + recognized_options = [ + "port", + "clock_freq", + "baudrate", + "chunk_size", + "stall_interval", + ] for option in config: if option not in recognized_options: warn( f"Ignoring unrecognized option '{option}' in UART interface config." ) - if "chunk_size" in config: - return cls(port, baudrate, clock_freq, config["chunk_size"]) - - else: - return cls(port, baudrate, clock_freq) + return cls(**config) def to_config(self): return { "port": self._port, "baudrate": self._baudrate, "clock_freq": self._clock_freq, + "stall_interval": self._stall_interval, "chunk_size": self._chunk_size, } @@ -204,9 +217,10 @@ class UARTInterface(Elaboratable): # Encode addrs into read requests bytes_out = "".join([f"R{a:04X}\r\n" for a in addr_chunk]) - # Add a \n after every 32 packets, see: + # Add a \n after every N packets, see: # https://github.com/fischermoseley/manta/issues/18 - bytes_out = "\n".join(split_into_chunks(bytes_out, 7 * 32)) + bytes_out = split_into_chunks(bytes_out, 7 * self._stall_interval) + bytes_out = "\n".join(bytes_out) ser.write(bytes_out.encode("ascii")) diff --git a/test/test_uart_baud_mismatch.py b/test/test_uart_baud_mismatch.py new file mode 100644 index 0000000..a625c72 --- /dev/null +++ b/test/test_uart_baud_mismatch.py @@ -0,0 +1,165 @@ +import os +from random import getrandbits + +import pytest +from amaranth import * +from amaranth.lib import io +from amaranth_boards.icestick import ICEStickPlatform +from amaranth_boards.nexys4ddr import Nexys4DDRPlatform + +from manta import * +from manta.utils import * + + +class UARTBaudrateMismatchTest(Elaboratable): + def __init__(self, platform, port, baudrate, percent_slowdown, stall_interval): + self.platform = platform + self.port = port + self.baudrate = baudrate + self.slowed_baudrate = baudrate * (1 - (percent_slowdown / 100)) + self.stall_interval = stall_interval + + def elaborate(self, platform): + # Since we know that all the tests will be called only after the FPGA + # is programmed, we can just push all the wiring into the elaborate + # method, instead of needing to define Manta in the __init__() method + + self.manta = manta = Manta() + manta.cores.mem = MemoryCore( + "bidirectional", + width=16, + depth=1024, + ) + + # Set the RTL to a slower baudrate. Later, we'll manually set the + # UARTInterface's _baudrate attribute back to the non-slowed baudrate + manta.interface = UARTInterface( + port=self.port, + baudrate=self.slowed_baudrate, + clock_freq=platform.default_clk_frequency, + stall_interval=self.stall_interval, + ) + + m = Module() + m.submodules.manta = self.manta + + uart_pins = platform.request("uart", dir={"tx": "-", "rx": "-"}) + m.submodules.uart_rx = uart_rx = io.Buffer("i", uart_pins.rx) + m.submodules.uart_tx = uart_tx = io.Buffer("o", uart_pins.tx) + + m.d.comb += self.manta.interface.rx.eq(uart_rx.i) + m.d.comb += uart_tx.o.eq(self.manta.interface.tx) + + return m + + def build_and_program(self): + self.platform.build(self, do_program=True) + + def fill_memory(self): + self.addrs = list(range(1024)) + self.datas = [getrandbits(16) for _ in self.addrs] + self.manta.cores.mem.write(self.addrs, self.datas) + + def verify_memory(self, batches): + datas = self.manta.cores.mem.read(self.addrs * batches) + if datas != (self.datas * batches): + raise ValueError("Data written does not match data read back!") + + def verify(self): + self.build_and_program() + + # Set the class back to the normal baudrate, which will be used when + # the port is opened + self.manta.interface._baudrate = self.baudrate + + # Write a bunch of data + self.fill_memory() + + # Read it back a few times, see if it's good + self.verify_memory(10) + + +# Nexys4DDR Tests +nexys4ddr_pass_cases = [ + (3e6, 0, 1024), # No clock mismatch, with no mitigation + (3e6, 0, 16), # No clock mismatch, with mitigation + (3e6, 1, 16), # Light clock mismatch, with light mitigation + (3e6, 2, 7), # Heavy clock mismatch, with heavy mitigation +] + + +@pytest.mark.skipif(not xilinx_tools_installed(), reason="no toolchain installed") +@pytest.mark.parametrize( + "baudrate, percent_slowdown, stall_interval", nexys4ddr_pass_cases +) +def test_baudrate_mismatch_xilinx_passes(baudrate, percent_slowdown, stall_interval): + UARTBaudrateMismatchTest( + platform=Nexys4DDRPlatform(), + port=os.environ["NEXYS4DDR_PORT"], + baudrate=baudrate, + percent_slowdown=percent_slowdown, + stall_interval=stall_interval, + ).verify() + + +nexys4ddr_fail_cases = [ + (3e6, 1, 1024), # Light clock mismatch, no mitigation + (3e6, 2, 1024), # Heavy clock mismatch, no mitigation + (3e6, 2, 16), # Heavy clock mismatch, light mitigation +] + + +@pytest.mark.skipif(not xilinx_tools_installed(), reason="no toolchain installed") +@pytest.mark.parametrize( + "baudrate, percent_slowdown, stall_interval", nexys4ddr_fail_cases +) +def test_baudrate_mismatch_xilinx_fails(baudrate, percent_slowdown, stall_interval): + with pytest.raises(ValueError, match="Only got"): + UARTBaudrateMismatchTest( + platform=Nexys4DDRPlatform(), + port=os.environ["NEXYS4DDR_PORT"], + baudrate=baudrate, + percent_slowdown=percent_slowdown, + stall_interval=stall_interval, + ).verify() + + +# IceStick Tests +ice40_pass_cases = [ + (115200, 0, 1024), # No clock mismatch, with no mitigation + (115200, 0, 16), # No clock mismatch, with mitigation + (115200, 1, 16), # Light clock mismatch, with light mitigation + (115200, 2, 7), # Heavy clock mismatch, with heavy mitigation +] + + +@pytest.mark.skipif(not ice40_tools_installed(), reason="no toolchain installed") +@pytest.mark.parametrize("baudrate, percent_slowdown, stall_interval", ice40_pass_cases) +def test_baudrate_mismatch_ice40_passes(baudrate, percent_slowdown, stall_interval): + UARTBaudrateMismatchTest( + platform=ICEStickPlatform(), + port=os.environ["ICESTICK_PORT"], + baudrate=baudrate, + percent_slowdown=percent_slowdown, + stall_interval=stall_interval, + ).verify() + + +ice40_fail_cases = [ + (115200, 1, 1024), # Light clock mismatch, no mitigation + (115200, 2, 1024), # Heavy clock mismatch, no mitigation + (115200, 2, 16), # Heavy clock mismatch, light mitigation +] + + +@pytest.mark.skipif(not ice40_tools_installed(), reason="no toolchain installed") +@pytest.mark.parametrize("baudrate, percent_slowdown, stall_interval", ice40_fail_cases) +def test_baudrate_mismatch_ice40_fails(baudrate, percent_slowdown, stall_interval): + with pytest.raises(ValueError, match="Only got"): + UARTBaudrateMismatchTest( + platform=ICEStickPlatform(), + port=os.environ["ICESTICK_PORT"], + baudrate=baudrate, + percent_slowdown=percent_slowdown, + stall_interval=stall_interval, + ).verify()