uart: add stall_interval parameter and tests

This commit is contained in:
Fischer Moseley 2024-09-14 12:47:18 -07:00
parent 7f36072e90
commit 60da631fa7
3 changed files with 194 additions and 12 deletions

View File

@ -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

View File

@ -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"))

View File

@ -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()