From 53538b4d58c81f98738bc0b8366db2c5b3b0bfc7 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 22 Aug 2025 17:21:57 -0600 Subject: [PATCH 1/2] gateware.uart: Create `ExternalUART` The previous `UART` implementation required being connected to physical pins, but this change allows specifying an arbitrary `UARTBus` implementation for `UART`s that make no assumptions about what they're connected to. --- .../glasgow/applet/interface/uart/__init__.py | 5 +- .../interface/uart_analyzer/__init__.py | 8 ++-- .../glasgow/applet/program/m16c/__init__.py | 2 +- .../glasgow/applet/sensor/pmsx003/__init__.py | 3 +- software/glasgow/gateware/uart.py | 48 +++++++++++-------- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/software/glasgow/applet/interface/uart/__init__.py b/software/glasgow/applet/interface/uart/__init__.py index c7f81bfc9..e0e961172 100644 --- a/software/glasgow/applet/interface/uart/__init__.py +++ b/software/glasgow/applet/interface/uart/__init__.py @@ -10,7 +10,7 @@ from glasgow.support.arepl import AsyncInteractiveConsole from glasgow.support.logging import dump_hex from glasgow.support.endpoint import ServerEndpoint -from glasgow.gateware.uart import UART +from glasgow.gateware.uart import ExternalUART from glasgow.abstract import AbstractAssembly, GlasgowPin from glasgow.applet import GlasgowAppletV2 @@ -117,7 +117,8 @@ def elaborate(self, platform): # TODO: `uart.bit_cyc` is only used to set the width of the register; the actual initial # value is zero (same as `self.bit_cyc`); this is a footgun and should be fixed by rewriting # the UART to use lib.wiring - m.submodules.uart = uart = UART(self.ports, + m.submodules.uart = uart = ExternalUART( + self.ports, bit_cyc=(1 << len(self.manual_cyc)) - 1, parity=self.parity) m.submodules.auto_baud = auto_baud = UARTAutoBaud() diff --git a/software/glasgow/applet/interface/uart_analyzer/__init__.py b/software/glasgow/applet/interface/uart_analyzer/__init__.py index b4c6d1859..2044adaeb 100644 --- a/software/glasgow/applet/interface/uart_analyzer/__init__.py +++ b/software/glasgow/applet/interface/uart_analyzer/__init__.py @@ -11,7 +11,7 @@ from amaranth.lib.wiring import In, Out from glasgow.gateware.ports import PortGroup -from glasgow.gateware.uart import UART +from glasgow.gateware.uart import ExternalUART from glasgow.gateware.stream import Queue from glasgow.abstract import AbstractAssembly, GlasgowPin, ClockDivisor from glasgow.applet import GlasgowAppletV2, GlasgowAppletError @@ -51,8 +51,10 @@ def elaborate(self, platform): channels = [] for index, pin in enumerate(self._port): - m.submodules[f"ch{index}"] = uart = UART(PortGroup(rx=pin), - bit_cyc=(1 << len(self.periods[index])) - 1, parity=self._parity) + m.submodules[f"ch{index}"] = uart = ExternalUART( + PortGroup(rx=pin), + bit_cyc=(1 << len(self.periods[index])) - 1, + parity=self._parity) m.d.comb += uart.bit_cyc.eq(self.periods[index] + 1) channels.append(uart) diff --git a/software/glasgow/applet/program/m16c/__init__.py b/software/glasgow/applet/program/m16c/__init__.py index e24b07be0..828f99220 100644 --- a/software/glasgow/applet/program/m16c/__init__.py +++ b/software/glasgow/applet/program/m16c/__init__.py @@ -80,7 +80,7 @@ def __init__(self, ports, out_fifo, in_fifo, bit_cyc, reset, mode, max_bit_cyc): self.reset = reset self.mode = mode - self.uart = UART(ports, bit_cyc=max_bit_cyc) + self.uart = ExternalUART(ports, bit_cyc=max_bit_cyc) def elaborate(self, platform): m = Module() diff --git a/software/glasgow/applet/sensor/pmsx003/__init__.py b/software/glasgow/applet/sensor/pmsx003/__init__.py index 4727cab1f..53702de56 100644 --- a/software/glasgow/applet/sensor/pmsx003/__init__.py +++ b/software/glasgow/applet/sensor/pmsx003/__init__.py @@ -27,7 +27,8 @@ def __init__(self, ports, in_fifo, out_fifo): def elaborate(self, platform): m = Module() - m.submodules.uart = uart = UART(self.ports, + m.submodules.uart = uart = ExternalUART( + self.ports, bit_cyc=int(platform.default_clk_frequency // 9600)) m.d.comb += [ self.in_fifo.w_data.eq(uart.rx_data), diff --git a/software/glasgow/gateware/uart.py b/software/glasgow/gateware/uart.py index e6b35e373..aaaa18911 100644 --- a/software/glasgow/gateware/uart.py +++ b/software/glasgow/gateware/uart.py @@ -3,7 +3,7 @@ from amaranth.lib.cdc import FFSynchronizer -__all__ = ["UART"] +__all__ = ["UART", "ExternalUART"] class UARTBus(Elaboratable): @@ -12,6 +12,7 @@ class UARTBus(Elaboratable): Provides synchronization. """ + def __init__(self, ports): self.ports = ports @@ -90,7 +91,8 @@ class UART(Elaboratable): Transmit acknowledgement. If active when ``tx_rdy`` is active, ``tx_rdy`` is reset, ``tx_data`` is sampled, and the transmit state machine starts transmitting a frame. """ - def __init__(self, ports, bit_cyc, data_bits=8, parity="none", max_bit_cyc=None): + + def __init__(self, bus, bit_cyc, data_bits=8, parity="none", max_bit_cyc=None): if max_bit_cyc is not None: self.max_bit_cyc = max_bit_cyc else: @@ -102,18 +104,18 @@ def __init__(self, ports, bit_cyc, data_bits=8, parity="none", max_bit_cyc=None) self.bit_cyc = Signal(range(self.max_bit_cyc + 1), init=bit_cyc) self.rx_data = Signal(data_bits) - self.rx_rdy = Signal() - self.rx_ack = Signal() + self.rx_rdy = Signal() + self.rx_ack = Signal() self.rx_ferr = Signal() self.rx_perr = Signal() - self.rx_ovf = Signal() - self.rx_err = Signal() + self.rx_ovf = Signal() + self.rx_err = Signal() self.tx_data = Signal(data_bits) - self.tx_rdy = Signal() - self.tx_ack = Signal() + self.tx_rdy = Signal() + self.tx_ack = Signal() - self.bus = UARTBus(ports) + self.bus = bus def elaborate(self, platform): m = Module() @@ -139,7 +141,7 @@ def calc_parity(sig, kind): if self.bus.has_rx: rx_start = Signal() rx_timer = Signal(range(self.max_bit_cyc)) - rx_stb = Signal() + rx_stb = Signal() rx_shreg = Signal(self.data_bits) rx_bitno = Signal(range(len(rx_shreg))) @@ -199,11 +201,11 @@ def calc_parity(sig, kind): ### if self.bus.has_tx: - tx_start = Signal() - tx_timer = Signal(range(self.max_bit_cyc)) - tx_stb = Signal() - tx_shreg = Signal(self.data_bits) - tx_bitno = Signal(range(len(tx_shreg))) + tx_start = Signal() + tx_timer = Signal(range(self.max_bit_cyc)) + tx_stb = Signal() + tx_shreg = Signal(self.data_bits) + tx_bitno = Signal(range(len(tx_shreg))) tx_parity = Signal() with m.If(tx_start | (tx_timer == 0)): @@ -222,7 +224,9 @@ def calc_parity(sig, kind): self.bus.tx_o.eq(0), ] if self.parity != "none": - m.d.sync += tx_parity.eq(calc_parity(self.tx_data, self.parity)) + m.d.sync += tx_parity.eq( + calc_parity(self.tx_data, self.parity) + ) m.next = "START" with m.Else(): m.d.sync += self.bus.tx_o.eq(1) @@ -230,7 +234,7 @@ def calc_parity(sig, kind): with m.If(tx_stb): m.d.sync += [ self.bus.tx_o.eq(tx_shreg[0]), - tx_shreg.eq(Cat(tx_shreg[1:], C(0,1))), + tx_shreg.eq(Cat(tx_shreg[1:], C(0, 1))), ] m.next = "DATA" with m.State("DATA"): @@ -239,7 +243,7 @@ def calc_parity(sig, kind): with m.If(tx_bitno != len(tx_shreg) - 1): m.d.sync += [ self.bus.tx_o.eq(tx_shreg[0]), - tx_shreg.eq(Cat(tx_shreg[1:], C(0,1))), + tx_shreg.eq(Cat(tx_shreg[1:], C(0, 1))), ] with m.Else(): if self.parity == "none": @@ -250,10 +254,16 @@ def calc_parity(sig, kind): m.next = "PARITY" with m.State("PARITY"): with m.If(tx_stb): - m.d.sync += self.bus.tx_o.eq(1), + m.d.sync += (self.bus.tx_o.eq(1),) m.next = "STOP" with m.State("STOP"): with m.If(tx_stb): m.next = "IDLE" return m + + +class ExternalUART(UART): + def __init__(self, ports, *args, **kwargs): + bus = UARTBus(ports) + super().__init__(bus, *args, **kwargs) From 83d192626f4f586b931ea4e99522d27bb796fc36 Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 25 Aug 2025 13:14:50 -0600 Subject: [PATCH 2/2] applet.interface: Create the uart-pinout applet Adds a uart-pinout applet that functions similar to the JTAGulator implementation for determining UART pinouts. Essentially this just sends data to the selected pins at various baud rates and looks for an echo. The default just sends a carriage return (0x0d) if no data is set, but the user can send arbitrary binary data up to 0xFF bytes long. --- .../applet/interface/uart_pinout/__init__.py | 473 ++++++++++++++++++ .../applet/interface/uart_pinout/test.py | 118 +++++ software/pyproject.toml | 1 + 3 files changed, 592 insertions(+) create mode 100644 software/glasgow/applet/interface/uart_pinout/__init__.py create mode 100644 software/glasgow/applet/interface/uart_pinout/test.py diff --git a/software/glasgow/applet/interface/uart_pinout/__init__.py b/software/glasgow/applet/interface/uart_pinout/__init__.py new file mode 100644 index 000000000..36b406af3 --- /dev/null +++ b/software/glasgow/applet/interface/uart_pinout/__init__.py @@ -0,0 +1,473 @@ +import asyncio +import logging + +from amaranth import Module, Mux, Signal, Cat, Const, unsigned +from amaranth.lib import stream, enum, io, wiring +from amaranth.lib.cdc import FFSynchronizer +from amaranth.lib.wiring import In, Out + +from glasgow.abstract import GlasgowPin +from glasgow.applet import GlasgowAppletError, GlasgowAppletV2 +from glasgow.gateware.uart import UART + +# This should be able to hold most sensible values +MAX_BAUD_WIDTH = 32 +# Keep sorted +_DEFAULT_BAUDS = [ + 9600, + 19200, + 38400, + 57600, + 115200, + 230400, + 460800, + 921600, +] + + +class _Parity(enum.Enum, shape=unsigned(2)): + NoParity = 0x00 + Even = 0x01 + Odd = 0x02 + + +class ModifiableUARTBus(wiring.Component): + """ + This class will expose the same interface as the UARTBus over in gateware, + but we just use Signals instead of ports since we're iterating over the + ports. Essentially the UART that uses this bus will be reading from + different pins at different times but always be using the same input/ + output + """ + + def __init__(self, ports): + + self._ports = ports + pincount = len(ports.pins) + + super().__init__( + { + # Leave all OE at 0 when this isn't enabled + "i_enable": In(1), + # Masks are used to select the pins for rx/tx + "i_tx_mask": In(pincount), + "i_rx_mask": In(pincount), + } + ) + + self.pin_values = Signal(pincount) + + # Implement UARTBus over in gateware, these fields are expected + self.rx_i = Signal(1) + self.has_rx = True + self.tx_o = Signal(1, init=1) + self.has_tx = True + + def elaborate(self, platform): + m = Module() + + pins = [] + + for i, port in enumerate(self._ports.pins): + m.submodules[f"pins{i}_buffer"] = pin = io.Buffer("io", port) + pins.append(pin) + + pincount = len(pins) + + self.pin_values = Signal(pincount) + m.submodules += FFSynchronizer(Cat(pin.i for pin in pins), self.pin_values) + + # TXOE will be used for the pin .oe values: setting a bit in TXOE enables + # the output on that bit. We use tx_mask to set the bit. + txoe = Signal(pincount) + # TX will be used to set the .o values. We basically just track tx_bit + # with this and use TXOE to determine which one should actually be enabled + tx = Signal(pincount) + + m.d.comb += [ + Cat(pin.oe for pin in pins).eq(txoe), + Cat(pin.o for pin in pins).eq(tx), + ] + + m.d.comb += self.rx_i.eq( + ((self.pin_values & self.i_rx_mask) != 0) | (self.i_rx_mask == 0) + ) + + ALL_PINS_HIGH = Const((1 << pincount) - 1, shape=tx.shape()) + ALL_PINS_LOW = Const(0, shape=tx.shape()) + + # tx is based on tx_o and the mask when enabled, note that we control + # .oe in the next line, so even though we're setting all pins not all + # have output enabled + m.d.comb += tx.eq( + Mux( + self.i_enable, + Mux(self.tx_o, ALL_PINS_HIGH, ALL_PINS_LOW), + ALL_PINS_HIGH, + ) + ) + + # txoe will only enable a pin when we are enabled, otherwise disable everything + m.d.comb += txoe.eq(Mux(self.i_enable, self.i_tx_mask, ALL_PINS_LOW)) + + return m + + +class UARTPinoutComponent(wiring.Component): + baud_ticks: In(MAX_BAUD_WIDTH) + nstopbits: In(4) + enable: In(1) + i_stream: In(stream.Signature(8)) + o_stream: Out(stream.Signature(8)) + + def __init__(self, ports, parity=_Parity.NoParity): + super().__init__() + self.bus = ModifiableUARTBus(ports) + + self.parity = ( + "none" + if parity == _Parity.NoParity + else ("even" if parity == _Parity.Even else "odd") + ) + + npins = len(ports.pins) + + self.rx_mask = Signal(npins) + self.tx_mask = Signal(npins) + + def elaborate(self, platform): + + m = Module() + + m.submodules.internal_uart = uart = UART( + self.bus, _DEFAULT_BAUDS[-1], parity=self.parity + ) + + m.d.comb += [ + self.bus.i_enable.eq(self.enable), + self.bus.i_rx_mask.eq(self.rx_mask), + self.bus.i_tx_mask.eq(self.tx_mask), + uart.bit_cyc.eq(self.baud_ticks), + uart.tx_data.eq(self.i_stream.payload), + uart.tx_ack.eq(self.i_stream.valid), + self.i_stream.ready.eq(uart.tx_rdy), + self.o_stream.payload.eq(uart.rx_data), + self.o_stream.valid.eq(uart.rx_rdy), + uart.rx_ack.eq(self.o_stream.ready), + ] + + return m + + +class UARTPinoutInterface: + def __init__(self, logger, assembly, *, pins, data=b"\x0d", rx_delay_sec=0.05): + self._logger = logger + self._sys_clk_period = assembly.sys_clk_period + self._trace(f"sys_clk_period[{self._sys_clk_period}]") + + self._ports = ports = assembly.add_port_group(pins=pins) + assembly.use_pulls({pins: "high"}) + self._component = component = assembly.add_submodule(UARTPinoutComponent(ports)) + + self._rx_delay_sec = rx_delay_sec + self._data = data + + self._rrx_mask = assembly.add_rw_register(component.rx_mask) + self._rtx_mask = assembly.add_rw_register(component.tx_mask) + self._renable = assembly.add_rw_register(component.enable) + self._rbaud_ticks = assembly.add_rw_register(component.baud_ticks) + # self._rparity = assembly.add_rw_register(component.parity) + self._rnstopbits = assembly.add_rw_register(component.nstopbits) + + self._pipe = assembly.add_inout_pipe( + component.o_stream, + component.i_stream, + ) + + def _log(self, lvl, msg, *args): + self._logger.log(lvl, "uart-pinout: " + msg, *args) + + def _dbg(self, msg, *args): + self._log(logging.DEBUG, msg, *args) + + def _err(self, msg, *args): + self._log(logging.ERROR, msg, *args) + + def _warn(self, msg, *args): + self._log(logging.WARN, msg, *args) + + def _trace(self, msg, *args): + self._log(logging.TRACE, msg, *args) + + def _info(self, msg, *args): + self._log(logging.INFO, msg, *args) + + def set_rx_delay_ms(self, ms): + self._rx_delay_sec = ms / 1000.0 + + def set_data(self, data): + self._data = data + + async def set_rx_pin(self, rx): + self._dbg(f"Setting RX pin to {rx}") + await self._rrx_mask.set(1 << rx) + + async def set_tx_pin(self, tx): + self._dbg(f"Setting TX pin to {tx}") + await self._rtx_mask.set(1 << tx) + + # async def set_parity(self, parity): + # await self._rparity.set(parity.value) + + async def set_nstopbits(self, nstopbits): + self._dbg(f"Setting nstopbits to {nstopbits}") + await self._rnstopbits.set(nstopbits) + + async def set_baud(self, baud): + # Convert the baud rate to the number of ticks of the system clock we need. + # There will be some error here, but I think it's generally not a big deal. + baud_ticks = round((1.0 / baud) / self._sys_clk_period) + self._trace(f"Setting baud to {baud}, ticks {baud_ticks}") + await self._rbaud_ticks.set(baud_ticks) + + async def try_baud(self, baud): + await self.set_baud(baud) + return await self.run() + + async def transact(self): + """ + Try to transmit the data at a given baud rate and listen for an echo + """ + + if not self._data: + raise GlasgowAppletError("try_baud called but empty data") + + self._dbg(f"Transacting data: {self._data}") + + await self._renable.set(1) + res = b"" + await self._pipe.send(self._data) + await self._pipe.flush() + + # We aren't guaranteed to get a perfect echo, so queue up reads one + # byte at a time + for _ in range(len(self._data)): + read = self._pipe.recv(1) + try: + data = await asyncio.wait_for(read, timeout=self._rx_delay_sec) + res += data + except TimeoutError: + self._trace(f"Timed out waiting for RX data, got {res}") + break + + await self._renable.set(0) + self._trace(f"Got res: {res}") + + return res or None + + +class UARTPinoutApplet(GlasgowAppletV2): + logger = logging.getLogger(__name__) + + help = "attemps to automatically determine UART pinout and baud rate" + description = """ + This applet works by simply sending data at various baud rates and waiting + for data back. If the UART you are testing does not ever echo data, this + cannot detect that UART! + """ + + @classmethod + def add_build_arguments(cls, parser, access): + access.add_voltage_argument(parser) + access.add_pins_argument(parser, "pins", width=range(2, 17), required=True) + + @classmethod + def add_run_arguments(cls, parser): + parser.add_argument( + "-d", "--data-hex", default="0d", help="Data to send as hex (default 0d)" + ) + parser.add_argument( + "-s", + "--data-ascii", + default=None, + help="Data to send as ASCII with \\ escapes valid", + ), + parser.add_argument( + "--rx-delay-ms", + default=0, + type=int, + help="Time to wait RX delay in ms, if not set this is determined from the chosen bauds", + ) + parser.add_argument( + "-b", + "--bauds", + default=None, + help="Comma separated list of bauds to try", + action="append", + ) + parser.add_argument( + "-e", + "--exclude-pins", + default=None, + action="append", + help="Comma separated list of pin numbers to exclude. Useful for using less pins without rebuilding the applet.", + ) + + parser.add_argument( + "-T", + "--tx", + default=None, + help="Set the TX pin and look for the RX pin", + ) + + parser.add_argument( + "-R", + "--rx", + default=None, + help="Set the RX pin and look for the TX pin", + ) + + def build(self, args): + + with self.assembly.add_applet(self): + self.assembly.use_voltage(args.voltage) + + self.uart_pinout_iface = UARTPinoutInterface( + self.logger, + self.assembly, + pins=args.pins, + ) + + def _dbg(self, msg, *args): + self.uart_pinout_iface._dbg(msg, *args) + + def _trace(self, msg, *args): + self.uart_pinout_iface._trace(msg, *args) + + def _info(self, msg, *args): + self.uart_pinout_iface._info(msg, *args) + + def calculate_abs_delay(self, bauds, data): + """ + Get a absolute delay that will account for the slowest baud rate + """ + + slowest = min(bauds) + # Assuming 1 start bit, 8 data bits, 1 stop bit and, for good measure, 1 parity bit + bits_per_byte = 11 + bits_for_data = bits_per_byte * len(data) + + longest_msg_time_ms = round((1000.0 / slowest) * bits_for_data) + # From some experimentation, it makes sense to set a lower bound here + wait_ms = max(50, longest_msg_time_ms * 2) + return wait_ms + + def get_data(self, args): + """ + Retrieve the command line provided data + """ + if args.data_ascii: + # Wow + return args.data_ascii.encode().decode("unicode_escape").encode() + try: + return bytes.fromhex(args.data_hex) + except ValueError: + raise GlasgowAppletError(f"invalid hex: {args.data_hex}") + + def _get_pin_idx(self, raw, pins, flag): + """ + Get the index into the pin array for the given GlasgowPin spec + """ + try: + pin = GlasgowPin.parse(raw)[0] + except (ValueError, IndexError): + raise GlasgowAppletError(f"Invalid pin spec for {flag}: {raw}") + + self._trace(f"Looking for pin {pin}") + + try: + return pins.index(pin) + except ValueError: + raise GlasgowAppletError(f"Exclude pin {raw} (from {flag}) not in pin set") + + + def _make_exclude_pins(self, exclude, pins): + indices = [] + for p in exclude: + indices.append(self._get_pin_idx(p, pins, "-e/--exclude-pins")) + return indices + + + async def run(self, args): + data = self.get_data(args) + bauds = ( + _DEFAULT_BAUDS + if not args.bauds + else sorted([int(e) for x in args.bauds for e in x.split(",")]) + ) + + for it in bauds: + if it.bit_length() > MAX_BAUD_WIDTH: + raise GlasgowAppletError( + f"invalid baud passed to --bauds, {it} has a larger bit width ({it.bit_length()}) than the max allowed value ({MAX_BAUD_WIDTH})" + ) + + pins = args.pins + npins = len(pins) + + if args.exclude_pins: + exclude = self._make_exclude_pins(args.exclude_pins, pins) + self._dbg(f"Excluding pins: {exclude}") + else: + exclude = [] + + abs_delay = args.rx_delay_ms + + if abs_delay == 0: + abs_delay = self.calculate_abs_delay(bauds, data) + self._dbg(f"Absolute delay set to {abs_delay}ms") + + if args.tx is not None: + tx_options = [self._get_pin_idx(args.tx, pins, "-T/--tx")] + else: + tx_options = range(npins) + + if args.rx is not None: + rx_options = [self._get_pin_idx(args.rx, pins, "-R/--rx")] + else: + rx_options = range(npins) + + self.uart_pinout_iface.set_rx_delay_ms(abs_delay) + self.uart_pinout_iface.set_data(data) + # The gateware UART only supports 1 stop bit + await self.uart_pinout_iface.set_nstopbits(1) + + for tx in tx_options: + + if tx in exclude: + continue + + await self.uart_pinout_iface.set_tx_pin(tx) + + for rx in rx_options: + + if rx == tx or rx in exclude: + continue + + await self.uart_pinout_iface.set_rx_pin(rx) + + for it in bauds: + await self.uart_pinout_iface.set_baud(it) + res = await self.uart_pinout_iface.transact() + if res: + if res == data: + print("** ", end="") + print( + f"TX[{pins[tx]}] RX[{pins[rx]}] BAUD[{it}] DATA[{res.hex()}]" + ) + + @classmethod + def tests(cls): + from . import test + + return test.UARTPinoutAppletTestCase diff --git a/software/glasgow/applet/interface/uart_pinout/test.py b/software/glasgow/applet/interface/uart_pinout/test.py new file mode 100644 index 000000000..7eb50a7a4 --- /dev/null +++ b/software/glasgow/applet/interface/uart_pinout/test.py @@ -0,0 +1,118 @@ +from amaranth import Elaboratable, Module +from glasgow.gateware.uart import ExternalUART +from glasgow.simulation.assembly import SimulationAssembly +from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test +from glasgow.applet.interface.uart_pinout import UARTPinoutApplet, UARTPinoutInterface + + +class UARTPinoutAppletTestCase(GlasgowAppletV2TestCase, applet=UARTPinoutApplet): + + @synthesis_test + def test_build(self): + self.assertBuilds(args=["--pins", "A0:3"]) + + def test_run(self): + + assembly = SimulationAssembly() + + pins = "A0:5" + baud = 115200 + baud_ticks = round((1.0 / baud) / assembly.sys_clk_period) + + iface = UARTPinoutInterface(assembly._logger, assembly, pins=pins) + + # Echo will just send RX to TX at 115200 baud. + class Echo(Elaboratable): + def elaborate(self, platform): + m = Module() + # Use B0 and B1 for the ports, we'll forward ports in A to them + # appropriately. + uart = ExternalUART( + ports=assembly.add_port_group(rx="B0", tx="B1"), + bit_cyc=baud.bit_length(), + ) + m.submodules.test_uart = uart + + b0 = assembly.get_pin("B0") + a5 = assembly.get_pin("A5") + + b1 = assembly.get_pin("B1") + a3 = assembly.get_pin("A3") + + m.d.comb += uart.bit_cyc.eq(baud_ticks) + + # B0 == A5 + m.d.comb += b0.i.eq((a5.o & a5.oe) | (~a5.oe)) + + # A3 == B1 + m.d.comb += a3.i.eq((b1.o & b1.oe) | (~b1.oe)) + + # TX == RX + m.d.comb += uart.tx_data.eq(uart.rx_data) + + with m.If(uart.rx_rdy): + m.d.comb += uart.tx_ack.eq(1) + m.d.comb += uart.rx_ack.eq(1) + + return m + + assembly.add_submodule(Echo()) + + async def testbench(ctx): + + # Set all test pins high since UARTs idle high, ignore A3 because + # we're driving that elsewhere. + for i in range(6): + if i == 3: + continue + pin = f"A{i}" + pin = assembly.get_pin(pin) + ctx.set(pin.i, 1) + + data = bytes.fromhex("0FF00FF0") + + await iface._rnstopbits.set(1) + iface.set_data(data) + # No need for a big delay with the test + iface.set_rx_delay_ms(1) + await iface.set_baud(baud) + + await iface.set_tx_pin(0) + await iface.set_rx_pin(1) + result = await iface.transact() + assert result is None, f"expected None got {result.hex()}" + + # Correct tx, but wrong rx + await iface.set_tx_pin(5) + result = await iface.transact() + assert result is None, f"expected None got {result.hex()}" + + # Correct rx, but wrong tx + await iface.set_tx_pin(2) + await iface.set_rx_pin(3) + result = await iface.transact() + assert result is None, f"expected None got {result.hex()}" + + # Both correct + await iface.set_tx_pin(5) + await iface.set_rx_pin(3) + result = await iface.transact() + assert result == data, f"expected {data.hex()} got {result.hex()}" + + # Cases with correct pins but incorrect bauds + await iface.set_tx_pin(5) + await iface.set_rx_pin(3) + + # Wrong baud (half) but correct pins + await iface.set_baud(int(baud / 2)) + result = await iface.transact() + expected = bytes.fromhex("F0F0") + assert result == expected, f"expected {expected.hex()} got {result.hex()}" + + # Wrong baud (double) but correct pins + await iface.set_baud(baud * 2) + result = await iface.transact() + expected = bytes.fromhex("F000") + assert result == expected, f"expected {expected.hex()} got {result.hex()}" + + assembly.run(testbench, vcd_file="test_uart_pinout_run.vcd") diff --git a/software/pyproject.toml b/software/pyproject.toml index c11517486..1ecf64287 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -90,6 +90,7 @@ benchmark = "glasgow.applet.internal.benchmark:BenchmarkApplet" analyzer = "glasgow.applet.interface.analyzer:AnalyzerApplet" uart = "glasgow.applet.interface.uart:UARTApplet" uart-analyzer = "glasgow.applet.interface.uart_analyzer:UARTAnalyzerApplet" +uart-pinout = "glasgow.applet.interface.uart_pinout:UARTPinoutApplet" spi-analyzer = "glasgow.applet.interface.spi_analyzer:SPIAnalyzerApplet" spi-controller = "glasgow.applet.interface.spi_controller:SPIControllerApplet" i2c-controller = "glasgow.applet.interface.i2c_controller:I2CControllerApplet"