diff --git a/docs/manual/src/_static/examples/assembly-applets.py b/docs/manual/src/_static/examples/assembly-applets.py new file mode 100644 index 000000000..a99071139 --- /dev/null +++ b/docs/manual/src/_static/examples/assembly-applets.py @@ -0,0 +1,50 @@ +# assembly-applets.py +# +# Instantiate two UART applets and add them to an Assembly. Then, trivially +# interact with them. +# +# To run this example, connect an external jumper cable between pins A0 and +# B0 on Glasgow. + +import asyncio +import logging + +from glasgow.hardware.assembly import HardwareAssembly +from glasgow.applet.interface.uart import UARTInterface + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +async def main(): + assembly = await HardwareAssembly.find_device() + + assembly.use_voltage({"A": 3.3, "B": 3.3}) + + # There is no inherent reason why these UARTs need to be on different + # banks -- you can have multiple applets on a single I/O bank. We do so + # in this example because it is convenient using to attach the banks + # together using Glasgow's included cabling. + uart_a = UARTInterface(logger, assembly, tx="A0", rx=None) + uart_b = UARTInterface(logger, assembly, tx=None, rx="B0") + + async with assembly: + logger.info("assembly has started") + await uart_a.set_baud(115200) + await uart_b.set_baud(115200) + + tx_data = b"Hello, Glasgow!" + + # Start these in parallel! + rx_task = asyncio.create_task(uart_b.read(len(tx_data))) + tx_task = asyncio.create_task(uart_a.write(tx_data, flush=True)) + + await tx_task + logger.info("uart_a transmitted data, waiting for received data") + + async with asyncio.timeout(5): + rx_data = await rx_task + logger.info(f"uart_b received data {bytes(rx_data)}") + assert tx_data == rx_data + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/_static/examples/assembly-logic.py b/docs/manual/src/_static/examples/assembly-logic.py new file mode 100644 index 000000000..982b3c836 --- /dev/null +++ b/docs/manual/src/_static/examples/assembly-logic.py @@ -0,0 +1,78 @@ +# assembly-logic.py +# +# Instantiate internal logic on Glasgow to blink the programmable LEDs. + +import asyncio +import logging + +from amaranth import * +from amaranth.lib import io +from glasgow.hardware.assembly import HardwareAssembly + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class BlinkLEDs(Elaboratable): + def elaborate(self, platform) -> Module: + # The returned Module encapsulates all of the logic that we will + # define. It also contains any submodules that we will instantiate. + m = Module() + + # In the future, we will pass I/O pins into a module using + # Amaranth's Wiring subsystem, but for now, we use the Amaranth + # Platform object that we were passed (which represents the Glasgow + # board we are running on), and we retrieve I/O pads associated with + # each of the five LEDs directly from there. + led_outs = [] + for n in range(5): + pad = platform.request("led", n, dir="-") + + # You cannot assign directly to a pad -- if you want to work + # with a pad, you need to instantiate an I/O buffer for it. The + # I/O buffer, in turn, has signals that you can assign later. + m.submodules[f"led_buffer_{n}"] = pad_buffer = io.Buffer("o", pad) + + led_outs.append(pad_buffer.o) + + # To make them easier to work with, we can concatenate together the + # five one-bit-wide LED pad outputs to form a 5-bit-wide signal. + led_out_bus = Cat(led_outs) + + # Experienced digital logic designers will realize that this infers + # a flipflop; in Glasgow's mental model, we are creating a signal, + # and then adding an assignment to that signal into the list for the + # synchronous control domain. In Verilog, this would be roughly + # equivalent to: + # + # reg [31:0] counter; + # always @(posedge sync_clk) + # counter <= counter + 1; + counter = Signal(32) + m.d.sync += counter.eq(counter + 1) + + # The "combinational" control domain is special, and statements + # added to that control domain take effect continuously. In + # Verilog, this would be roughly equivalent to: + # + # always @(*) + # led_out_bus = counter[27:23]; + # + # (Note that bit indices in Amaranth follow Python MSB-exclusive + # array index convention, rather than Verilog MSB-inclusive + # convention!) + m.d.comb += led_out_bus.eq(counter[23:28]) + + return m + +async def main(): + assembly = await HardwareAssembly.find_device() + + assembly.add_submodule(BlinkLEDs()) + + async with assembly: + logger.info("assembly has started") + while True: + await asyncio.sleep(1) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/_static/examples/assembly-skeleton.py b/docs/manual/src/_static/examples/assembly-skeleton.py new file mode 100644 index 000000000..64e9ddd70 --- /dev/null +++ b/docs/manual/src/_static/examples/assembly-skeleton.py @@ -0,0 +1,18 @@ +# assembly-skeleton.py + +import asyncio +import logging + +from glasgow.hardware.assembly import HardwareAssembly + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +async def main(): + assembly = await HardwareAssembly.find_device() + async with assembly: + logger.info("Glasgow is alive!") + await asyncio.sleep(5) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/_static/examples/using-pins.py b/docs/manual/src/_static/examples/using-pins.py new file mode 100644 index 000000000..bbf61bc79 --- /dev/null +++ b/docs/manual/src/_static/examples/using-pins.py @@ -0,0 +1,108 @@ +# using-pins.py +# +# Instantiate an external-loopback path on Glasgow using input and output +# buffers. + +import asyncio +import logging + +from amaranth import * +from amaranth.lib import wiring, io +from amaranth.lib.cdc import FFSynchronizer +from amaranth.lib.wiring import In, Out +from glasgow.hardware.assembly import HardwareAssembly + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class DrivePorts(wiring.Component): + tx_data: In(4) + rx_data: Out(4) + + # The In- and Out-typed properties in a Component inform the Component's + # signature, but you can of course have any other type of property in a + # Component, and the Component's constructor will not automatically + # attach Signals to them. Here, we define instance properties for the + # pins that we will later connect. + _tx_port: io.PortLike + _rx_port: io.PortLike + + def __init__(self, tx_port, rx_port): + # Traditionally, ports (and other parameterizations of a Component) + # are passed in through a constructor, rather than being assigned to + # instance properties externally. This allows the Component's + # constructor to validate its usage. + assert len(tx_port) == 4 + assert len(rx_port) == 4 + + self._tx_port = tx_port + self._rx_port = rx_port + + super().__init__() + + def elaborate(self, platform) -> Module: + m = Module() + + # Much like the LED drivers, we instantiate I/O buffers for the pins + # that we are passed. + m.submodules.tx_buf = tx_buf = io.Buffer("o", self._tx_port) + m.submodules.rx_buf = rx_buf = io.Buffer("i", self._rx_port) + + # Although the cone of logic is simple here, we generally prefer to + # drive output pins directly from the output pins of flops + # (expressed here with the "sync" control domain). Doing so means + # that pins that drive external asynchronous devices will change at + # most once per clock, rather than glitching as internal logic paths + # propagate. + m.d.sync += tx_buf.o.eq(~self.tx_data) + + # To avoid metastability, it is good practice to synchronize inputs + # coming from outside the chip into a clock domain. (In this + # example, it is not strictly necessary, because the input pins + # should be connected to the output pins from the same clock domain! + # But the pins do leave the physical Glasgow package, and could + # theoretically be connected to anything, so we synchronize them in + # to be sure.) + m.submodules.sync_rx = FFSynchronizer(rx_buf.i, self.rx_data) + + return m + +async def main(): + assembly = await HardwareAssembly.find_device() + + # Again, there is no inherent reason why these need to be on different + # ports; we do so in this example to make it easier to connect Glasgow + # in the specified way using the wiring in the package. + assembly.use_voltage({"A": 3.3, "B": 3.3}) + + # The usage of "assembly.add_port" in this example is very similar to + # the "platform.request" API in the previous examples. + driver = DrivePorts( + tx_port=assembly.add_port(pins=("A0", "A1", "A2", "A3"), name="tx"), + rx_port=assembly.add_port(pins=("B0", "B1", "B2", "B3"), name="rx") + ) + assembly.add_submodule(driver) + + # In this example, we instantiate not just a read-write + # (host-to-Glasgow) register, but a read-only (Glasgow-to-host) + # register. We connect them from the harness into the signals in the + # module. + tx_reg = assembly.add_rw_register(driver.tx_data) + rx_reg = assembly.add_ro_register(driver.rx_data) + + async with assembly: + logger.info("assembly has started") + + # Write a sequence of four-bit values to the pins by writing their + # register, and then read back each time to make sure that we + # receive what we expected. + for i in range(16): + await tx_reg.set(i) + rv = await rx_reg.get() + + expected = i ^ 0xF + logger.info(f"transmitted {i:x}, received {rv:x} (expected {expected:x})") + assert rv == expected + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/_static/examples/using-pipes.py b/docs/manual/src/_static/examples/using-pipes.py new file mode 100644 index 000000000..5fe012e7d --- /dev/null +++ b/docs/manual/src/_static/examples/using-pipes.py @@ -0,0 +1,142 @@ +# using-pipes.py +# +# Instantiate a higher bandwidth external-loopback path on Glasgow, using +# input and output buffers that are connected to pipes. +import asyncio +import logging + +from amaranth import * +from amaranth.lib import wiring, io, stream +from amaranth.lib.cdc import FFSynchronizer +from amaranth.lib.wiring import In, Out +from glasgow.hardware.assembly import HardwareAssembly + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class PipesToPorts(wiring.Component): + tx_stream: In(stream.Signature(8)) # Pipes are always 8-bits. + rx_stream: Out(stream.Signature(8)) + + rx_received: Out(16) + rx_overflow_count: Out(16) + + _tx_port: io.PortLike + _rx_port: io.PortLike + + def __init__(self, tx_port, rx_port): + assert len(tx_port) == 4 + assert len(rx_port) == 4 + + self._tx_port = tx_port + self._rx_port = rx_port + + super().__init__() + + def elaborate(self, platform) -> Module: + m = Module() + + m.submodules.tx_buf = tx_buf = io.Buffer("o", self._tx_port) + + m.d.comb += self.tx_stream.ready.eq(1) + + m.d.sync += tx_buf.o[0].eq(self.tx_stream.valid) + m.d.sync += tx_buf.o[1:4].eq(~self.tx_stream.payload[0:3]) + + m.submodules.rx_buf = rx_buf = io.Buffer("i", self._rx_port) + + rx_data = Signal(4) + m.submodules.sync_rx = FFSynchronizer(rx_buf.i, rx_data) + + m.d.comb += self.rx_stream.valid.eq(rx_data[0]) + m.d.comb += self.rx_stream.payload.eq(rx_data[1:4]) + + with m.If(self.rx_stream.valid): + m.d.sync += self.rx_received.eq(self.rx_received + 1) + + with m.If(~self.rx_stream.ready): + m.d.sync += self.rx_overflow_count.eq(self.rx_overflow_count + 1) + + return m + +async def main(): + assembly = await HardwareAssembly.find_device() + + # Again, there is no inherent reason why these need to be on different + # ports; we do so in this example to make it easier to connect Glasgow + # in the specified way using the wiring in the package. + assembly.use_voltage({"A": 3.3, "B": 3.3}) + + driver = PipesToPorts( + tx_port=assembly.add_port(pins=("A0", "A1", "A2", "A3"), name="tx"), + rx_port=assembly.add_port(pins=("B0", "B1", "B2", "B3"), name="rx") + ) + assembly.add_submodule(driver) + + rx_received = assembly.add_ro_register(driver.rx_received) + rx_overflow_count = assembly.add_ro_register(driver.rx_overflow_count) + + tx_pipe = assembly.add_out_pipe(driver.tx_stream, fifo_depth = 8) + rx_pipe = assembly.add_in_pipe(driver.rx_stream, fifo_depth = 16, buffer_size = 32) + + async with assembly: + logger.info("assembly has started") + + # The applet might not have been reset, if it was cached and + # previously running on Glasgow, so these registers may have a + # nonzero initialization value. + rx_received_initial = await rx_received.get() + rx_overflow_count_initial = await rx_overflow_count.get() + + logger.info("pipes: testing the good case of receiving the number of bytes we send") + rx_task = asyncio.create_task(rx_pipe.recv(8)) + await tx_pipe.send(b'\x00\x01\x02\x03\x04\x05\x06\x07') + await tx_pipe.flush() + rx_data = await rx_task + + assert rx_data == b'\x07\x06\x05\x04\x03\x02\x01\x00' + assert (await rx_received.get() - rx_received_initial) == 8 + assert (await rx_overflow_count.get() - rx_overflow_count_initial) == 0 + + logger.info("pipes: testing the bad case: sending too much data, and too quickly!") + await tx_pipe.send(b'\x00' * 256) + await tx_pipe.flush() + + assert (await rx_received.get() - rx_received_initial) == (8 + 256) + + # There are a few valid options here. There should be, at most, + # 256-16 words that got overflowed -- that would happen if the + # fifo_depth filled up immediately, and no words got popped out by + # the host before we finished pushing. Or, at the very least, we + # should have sent not more than the buffer_size + fifo_depth worth + # of words, when the host would fill up its software buffer and stop + # receiving. + # + # On implementations current as of the time of this writing, the USB + # flush logic results in 256-16 words beint dropped. + overflowed = await rx_overflow_count.get() - rx_overflow_count_initial + assert overflowed >= 256 - 16 - 32 and overflowed <= 256 - 16 + + # Flush all remaining data in the internal buffer (happens + # immediately), and any data in the Glasgow hardware FIFO (which + # might take a little bit to come in, since there may have been data + # still in Glasgow's FIFO waiting for space in the software buffer). + try: + while True: + async with asyncio.timeout(0.25): + await rx_pipe.recv(rx_pipe.readable or 1) + except TimeoutError: + pass + + logger.info("pipes: testing to be sure we are back in sync") + rx_task = asyncio.create_task(rx_pipe.recv(4)) + await tx_pipe.send(b'\x07\x00\x07\x00') + await tx_pipe.flush() + rx_data = await rx_task + assert rx_data == b'\x00\x07\x00\x07' + + tx_pipe.statistics() + rx_pipe.statistics() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/_static/examples/using-registers.py b/docs/manual/src/_static/examples/using-registers.py new file mode 100644 index 000000000..3af4dc336 --- /dev/null +++ b/docs/manual/src/_static/examples/using-registers.py @@ -0,0 +1,66 @@ +# assembly-logic.py +# +# Instantiate internal logic on Glasgow to blink the programmable LEDs. + +import asyncio +import logging + +from amaranth import * +from amaranth.lib import wiring, io +from amaranth.lib.wiring import In, Out +from glasgow.hardware.assembly import HardwareAssembly + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class DriveLEDs(wiring.Component): + led_data: In(5) + + def elaborate(self, platform) -> Module: + # Components are just Elaboratables -- they still return a Module + # that encapsulates their logic. + m = Module() + + # We build the LED pad buffers in the same way as we did in the + # previous example. + led_outs = [] + for n in range(5): + pad = platform.request("led", n, dir="-") + m.submodules[f"led_buffer_{n}"] = pad_buffer = io.Buffer("o", pad) + led_outs.append(pad_buffer.o) + led_out_bus = Cat(led_outs) + + # Now that we're controlling the LEDs from the host side, we don't + # need to instantiate a counter of our own -- instead, we just + # instantiate a flop stage between the data driven into this + # Component and the LED output. + m.d.sync += led_out_bus.eq(self.led_data) + + return m + +async def main(): + assembly = await HardwareAssembly.find_device() + + # Now that the module has properties that we care about, we need to keep + # a name for the module around! + leds = DriveLEDs() + assembly.add_submodule(leds) + + # Because the "led_data" input of our module just turns into a Signal, + # we can pass that to anything else that will assign to it. Here, we + # use the Assembly's mechanism for connecting signals through to the + # host -- a `RWRegister`, and we hand it the signal from our module to + # drive bits onto. + data_port = assembly.add_rw_register(leds.led_data) + + async with assembly: + logger.info("assembly has started") + while True: + # Now, in our inner loop, we can update the contents of the data + # register -- which then get sent to the data signal inside our + # module, and in turn to the LEDs. + await data_port.set(await data_port.get() + 1) + await asyncio.sleep(0.1) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/manual/src/embedding/adding-an-applet.rst b/docs/manual/src/embedding/adding-an-applet.rst new file mode 100644 index 000000000..f75ef0623 --- /dev/null +++ b/docs/manual/src/embedding/adding-an-applet.rst @@ -0,0 +1,74 @@ +Adding an applet to an Assembly +------------------------------- + +The most common and straightforward use of an Assembly is to add `existing Glasgow +applets <../applets>`_ to it. (Indeed, internally, the "new-style" Glasgow +``AppletV2`` subsystem is based on Assemblies.) Many Glasgow applets were +designed to be used with :ref:`the REPL `, and expose a +programmatic interface to be used interactively; you can also use these from +your own programs. In this section, we will instantiate a pair of UARTs, +with the interface as described in :ref:`the UART REPL example `. + +.. note:: + + Not all Glasgow applets have been ported to the "new-style" API yet. + (Applets that haven't instead derive from ``GlasgowApplet``.) If + you find one that hasn't yet been ported, the Glasgow project will + gladly accept your help! + +Most "new-style" applets include an ``Interface`` module that encapsulates +their digital logic, and host-side logic to act on it. The UART is no +exception; it is implemented as +``glasgow.applet.interface.uart.UARTInterface``. In this example, we +instantiate two ``UARTInterface``\s, and use them to talk to each other +through Glasgow's external I/O pins. Most of the example is relatively +self-explanatory, but it is worth considering: + +* Instantiating the ``Interface`` -- and, indeed, any module that adds logic + into the Assembly -- must be done before the Assembly is started. In our + examples, as described above, the Assembly is started implicitly by the + ``async with`` block, so we attach the ``UARTInterface`` to the Assembly + before we enter that block. +* Conversely, interacting with the ``Interface`` can happen only after + synthesis is complete and the gateware is running to Glasgow. Many + applets will implement configuration settings (in this example, setting + the baud rate on the UART peripheral) as dynamic register writes; these + qualify as interactions, for our purposes! So we ``set_baud`` on each of + the ``UARTInterface``\s inside of the ``async with`` block, after the + Assembly has been started. +* In this example, we want to run the transmit and receive tasks in parallel + (the Glasgow system has enough buffer for this trivial case, even if we do + not, but it is educational to demonstrate how to do it!). Many + applications will want to operate in a "straight line" -- there is no + inherent requirement that ``uart_b.read(...)`` must be wrapped in an + ``asyncio.create_task``, and indeed, you could just as well do something + like ``result = await uart_b.read(...)`` to immediately block on an + interaction with an ``Interface``. (This is also demonstrated in the + ``.set_baud`` calls.) + +`Below, we give a program <../_static/examples/assembly-applets.py>`_ that +instantiates two unidirectional UARTs, sets them each to 115200 baud, and +transmits some bytes from one to the other. In order to run this program, +remember to connect a flying lead from pin A0 to pin B0! + +.. literalinclude:: ../_static/examples/assembly-applets.py + :language: python + +Glasgow should respond: + +.. code:: console + + DEBUG:asyncio:Using selector: EpollSelector + DEBUG:glasgow.hardware.device:found revC3 device with serial C3-20240518T200308Z + DEBUG:glasgow.hardware.assembly:setting port A voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:setting port B voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:assigning pin tx[0] to A0 + DEBUG:glasgow.hardware.assembly:assigning pin rx[0] to B0 + DEBUG:glasgow.hardware.assembly:pulling pin B0 high + DEBUG:glasgow.hardware.toolchain:using toolchain 'builtin' (yosys 0.61.0.0.post1073, nextpnr-ice40 0.9.0.0.post686, icepack 0.9.0.0.post686) + INFO:glasgow.hardware.device:device already has bitstream ID 083ca04cc3edb43de9ba63d35bec38fc + INFO:glasgow.hardware.assembly:port A voltage set to 3.3 V + INFO:glasgow.hardware.assembly:port B voltage set to 3.3 V + INFO:root:assembly has started + INFO:root:uart_a transmitted data, waiting for received data + INFO:root:uart_b received data b'Hello, Glasgow!' diff --git a/docs/manual/src/embedding/adding-your-own-logic.rst b/docs/manual/src/embedding/adding-your-own-logic.rst new file mode 100644 index 000000000..54b61a6ea --- /dev/null +++ b/docs/manual/src/embedding/adding-your-own-logic.rst @@ -0,0 +1,73 @@ +Putting your own logic into an Assembly +--------------------------------------- + +One of the most important parts of the Glasgow system is that it is built +off of programmable logic. This means that even if none of the existing +Glasgow applets meet your needs, you can design digital logic to implement +protocols or analyzers that you find useful. In this section, we will +build a simple binary counter that blinks Glasgow's LEDs. + +The Glasgow system is based off of the `Amaranth hardware description +language `_, which is a +domain-specific language embedded in Python. This section provides a very +simple introduction to specifying extremely trivial hardware in Amaranth, +but experienced digital logic designers may find it useful to `read the +Amaranth language guide +`_ + +In the previous example, the ``UARTInterface`` that was instantiated was +responsible for adding itself to the Assembly. Internally, the +``UARTInterface`` created a Python object that `implemented the +Elaboratable abstract base class +`_, and then called the Assembly's +``add_submodule()`` method; in order to implement our own logic, we will +want to create our own ``Elaboratable`` object. + +.. note:: + + This is probably the only time you will ever directly instantiate an + ``Elaboratable``! In general, if you find yourself directly + instantiating one, you are probably doing something very unusual. + Later, when we connect pins and registers to our logic, we'll switch to + the more powerful ``wiring.Component`` -- itself a subclass of an + ``Elaboratable`` -- which allows us to describe, roughly, "things that + you can connect together", on top of the ``Elaboratable``'s abstraction + of "things that contain logic". But for now, since the logic we're + about to describe keeps all of its I/Os internally, we won't quite + concern ourselves with the ``wiring.Component`` yet. + +All ``Elaboratable`` objects have a method, ``elaborate``, that instantiates +the logic inside of a module. (Digital logic designers will recognize the +contents as being the RTL that they are used to writing!) This example +mostly documents itself within its comments, but to solidify your +understanding, you may wish to consider the following exercises: + +* What happens to the LEDs when you press Ctrl-C to exit the Glasgow + runtime, and why? Should the ``asyncio.sleep`` loop actually be + necessary? Why, or why not? +* What if you wanted to make all five LEDs blink at the same time? Normal + Python control flow (i.e., ``if counter[23] == 0:``) will not behave as + you might expect -- why not? How many different ways can you come up with + to express the behavior of making all five LEDs blink at the same time? + +`Below, we give a program <../_static/examples/assembly-logic.py>`_ that +implements a binary counter on the LEDs. You do not need any external +connections other than the Glasgow hardware itself to run this program. + +.. literalinclude:: ../_static/examples/assembly-logic.py + :language: python + +Glasgow should respond: + +.. code:: console + + DEBUG:asyncio:Using selector: EpollSelector + DEBUG:glasgow.hardware.device:found revC3 device with serial C3-20240518T200308Z + DEBUG:glasgow.hardware.assembly:setting port A voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:setting port B voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:assigning pin tx[0] to A0 + DEBUG:glasgow.hardware.assembly:assigning pin rx[0] to B0 + DEBUG:glasgow.hardware.assembly:pulling pin B0 high + DEBUG:glasgow.hardware.toolchain:using toolchain 'builtin' (yosys 0.61.0.0.post1073, nextpnr-ice40 0.9.0.0.post686, icepack 0.9.0.0.post686) + INFO:glasgow.hardware.device:device already has bitstream ID c85af25c02e4e3f0088bf92d4ce7310c + INFO:root:assembly has started diff --git a/docs/manual/src/embedding/getting-started-with-assemblies.rst b/docs/manual/src/embedding/getting-started-with-assemblies.rst new file mode 100644 index 000000000..a3d0e6887 --- /dev/null +++ b/docs/manual/src/embedding/getting-started-with-assemblies.rst @@ -0,0 +1,60 @@ +Getting started with Assemblies +------------------------------- + +The core concept of the Glasgow embeddable API is the Assembly, accessible +through the ``glasgow.hardware.assembly.HardwareAssembly`` class +[#other_assemblies]_. An Assembly represents a configuration of gateware +for a specific Glasgow, including all gateware necessary to interface with +the host software, and including all pipes and registers that the gateware +has access to. + +Because each Assembly object is associated with a specific Glasgow, in order +to begin working with an Assembly, you will need to instantiate it with +reference to an attached device; you can use +``HardwareAssembly.find_device()`` to locate a device, and build an Assembly +based on it. An Assembly has ``.start()`` and ``.stop()`` methods to +synthesize it and download it to the device, but for convenience, it also +implements the async context manager protocol to connect to the device. +`The following skeleton of a program +<../_static/examples/assembly-skeleton.py>`_ will search for a Glasgow, +create an empty Assembly targetted to it, and then download it to the +attached Glasgow: + +.. literalinclude:: ../_static/examples/assembly-skeleton.py + :language: python + +.. note:: + + Most users that have `followed the recommended installation instructions + `__ will have Glasgow already installed via ``pipx``. + Usually, this is an important part of making Glasgow easy-to-install -- + but ``pipx`` is designed for standalone packages that are not meant to + be imported! Installing Glasgow outside of ``pipx`` in your own + environment is outside of the scope of this document, but to run these + samples, you might consider running inside of the Glasgow venv that + ``pipx`` already set up for you. For many users, doing so will take the + form: + + .. code:: console + + $ ~/.local/pipx/venvs/glasgow/bin/python3 assembly-skeleton.py + +Glasgow should respond: + +.. code:: console + + DEBUG:asyncio:Using selector: EpollSelector + DEBUG:glasgow.hardware.device:found revC3 device with serial C3-20240518T200308Z + DEBUG:glasgow.hardware.toolchain:using toolchain 'builtin' (yosys 0.61.0.0.post1073, nextpnr-ice40 0.9.0.0.post686, icepack 0.9.0.0.post686) + INFO:glasgow.hardware.device:generating bitstream ID ae08e17ee60fe32bc1165e0c59410d57 + DEBUG:glasgow.hardware.build_plan:bitstream ID ae08e17ee60fe32bc1165e0c59410d57 is not cached, executing build + INFO:root:Glasgow is alive! + +.. [#other_assemblies] + + There are other Assemblies in Glasgow; for instance, if you + wish to develop gateware without hardware on your desk at all, you might + consider a ``SimulationAssembly``. All Assemblies derive from the + ``AbstractAssembly`` base class, which is the type that you will most + commonly find passed around in Glasgow's internals. These types of + Assemblies are out of scope for this document! diff --git a/docs/manual/src/embedding/index.rst b/docs/manual/src/embedding/index.rst new file mode 100644 index 000000000..cfdb45b90 --- /dev/null +++ b/docs/manual/src/embedding/index.rst @@ -0,0 +1,40 @@ +Embedding Glasgow +================= + +For some applications, it may be either inconvenient or inappropriate to use +the Glasgow applet interface. For instance, you may wish to build a class +of interface for Glasgow that will not ever end up in Glasgow upstream (and +therefore should not be written inside of the Glasgow repository); you may +wish to control Glasgow in another program's main loop; or you may wish to +write a quick one-off experiment without the full infrastructure of Glasgow. + +Such applications may prefer to use Glasgow's embeddable APIs, which provide +access to directly find and instantiate Glasgow hardware, to build gateware +and host-side interfaces, and to load and run the generated programs on +Glasgow hardware. The embeddable APIs are designed to provide a similar +level of abstraction as the Glasgow applet interface (the embeddable API is +responsible for generating and loading bitstreams, and providing physical +communications with Glasgow), but control flow is owned by the host +application; Glasgow's embeddable APIs run in an ordinary Python `asyncio` +framework. + +.. caution:: + + Glasgow's embeddable APIs are an implementation artifact of the existing + applet architecture, which is the only committed interface for Glasgow. + All applets inside the Glasgow repository will be maintained and pushed + forward when internal APIs change -- but if you develop against + Glasgow's embeddable APIs, they may change underneath you in future + versions of Glasgow! + +This section documents Glasgow's embeddable APIs in a tutorial fashion. + +.. toctree:: + + getting-started-with-assemblies + adding-an-applet + adding-your-own-logic + using-registers + using-pins + using-pipes + diff --git a/docs/manual/src/embedding/using-pins.rst b/docs/manual/src/embedding/using-pins.rst new file mode 100644 index 000000000..a4a0e3b63 --- /dev/null +++ b/docs/manual/src/embedding/using-pins.rst @@ -0,0 +1,79 @@ +Connecting to pins +------------------ + +Glasgow's interface between digital logic and host software is certainly +lovely, but let's face it -- if you only wanted to blink LEDs, you wouldn't +bother to buy hardware, and you'd just `cosimulate in the Amaranth +playground `_! The core of what you want +to do with Glasgow is use the I/O pins to interface with other circuits. + +We previously instantiated output buffers for the platform LEDs, and +connected those to a host-writable register. In this example, we'll connect +the writable register to output buffers attached to physical pins, and we'll +also instantiate some input buffers associated with host-readable registers. + +On Glasgow, you access the pins on the board by requesting them from the +``Assembly``. One way to access pins is by requesting individual pins in +sequence (or a sequential bus of pins, as shown in this example): we use the +``assembly.get_port`` API for this, which returns an ``io.PortLike`` +(similar to the LED pads that we requested in the previous example). It can +be convenient to request many different pins all in one operation; to do so, +take a look at the ``assembly.get_port_group`` API. + +The Glasgow ``Assembly`` mechanism abstracts the implementation details of +any given version of Glasgow's interface to its pads -- whether a device +uses the FPGA's internal output buffers for tristatable drivers, or whether +it (like Glasgow revC) uses external level shifters with their own output +enable pins, the ``PortLike`` API encapsulates these differences so that +your code will continue to work on any Glasgow-like device that has +appropriately labeled pins. Similarly, instead of specifying FPGA pin +numbers, the Assembly maps ports to user-visible labeled pins; no matter +what the underlying FPGA is on a device, you can always specify pin ``"A0"`` +to get the first pin in port A. + +`Below, we give a program <../_static/examples/using-pins.py>`_ that inverts +a sequence of values as they are written to port A, and then receives them +on port B. The host driver verifies that the received values are the +expected inverted written values. In order to run this program, remember to +connect flying leads from pins A0 to B0, A1 to B1, A2 to B2, and A3 to B3! + +.. literalinclude:: ../_static/examples/using-pins.py + :language: python + +Glasgow should respond: + +.. code:: console + + DEBUG:asyncio:Using selector: EpollSelector + DEBUG:glasgow.hardware.device:found revC3 device with serial C3-20240518T200308Z + DEBUG:glasgow.hardware.assembly:setting port A voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:setting port B voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:assigning pin tx[0][0] to A0 + DEBUG:glasgow.hardware.assembly:assigning pin tx[1][0] to A1 + DEBUG:glasgow.hardware.assembly:assigning pin tx[2][0] to A2 + DEBUG:glasgow.hardware.assembly:assigning pin tx[3][0] to A3 + DEBUG:glasgow.hardware.assembly:assigning pin rx[0][0] to B0 + DEBUG:glasgow.hardware.assembly:assigning pin rx[1][0] to B1 + DEBUG:glasgow.hardware.assembly:assigning pin rx[2][0] to B2 + DEBUG:glasgow.hardware.assembly:assigning pin rx[3][0] to B3 + DEBUG:glasgow.hardware.toolchain:using toolchain 'builtin' (yosys 0.61.0.0.post1073, nextpnr-ice40 0.9.0.0.post686, icepack 0.9.0.0.post686) + INFO:glasgow.hardware.device:device already has bitstream ID 152e59126139a752bf89f6c498eac037 + INFO:glasgow.hardware.assembly:port A voltage set to 3.3 V + INFO:glasgow.hardware.assembly:port B voltage set to 3.3 V + INFO:root:assembly has started + INFO:root:transmitted 0, received f (expected f) + INFO:root:transmitted 1, received e (expected e) + INFO:root:transmitted 2, received d (expected d) + INFO:root:transmitted 3, received c (expected c) + INFO:root:transmitted 4, received b (expected b) + INFO:root:transmitted 5, received a (expected a) + INFO:root:transmitted 6, received 9 (expected 9) + INFO:root:transmitted 7, received 8 (expected 8) + INFO:root:transmitted 8, received 7 (expected 7) + INFO:root:transmitted 9, received 6 (expected 6) + INFO:root:transmitted a, received 5 (expected 5) + INFO:root:transmitted b, received 4 (expected 4) + INFO:root:transmitted c, received 3 (expected 3) + INFO:root:transmitted d, received 2 (expected 2) + INFO:root:transmitted e, received 1 (expected 1) + INFO:root:transmitted f, received 0 (expected 0) diff --git a/docs/manual/src/embedding/using-pipes.rst b/docs/manual/src/embedding/using-pipes.rst new file mode 100644 index 000000000..8e8d9e0d9 --- /dev/null +++ b/docs/manual/src/embedding/using-pipes.rst @@ -0,0 +1,2 @@ +Using pipes to transfer data +---------------------------- diff --git a/docs/manual/src/embedding/using-registers.rst b/docs/manual/src/embedding/using-registers.rst new file mode 100644 index 000000000..c16dce791 --- /dev/null +++ b/docs/manual/src/embedding/using-registers.rst @@ -0,0 +1,74 @@ +Using registers to connect to logic +----------------------------------- + +Once we have gained the capability to dynamically generate digital logic for +Glasgow, the rest of the power of Glasgow lies in its easy integration with +software running on the host computer. + +In addition to user logic, the Glasgow ``Assembly`` system automatically +instantiates a communications framework to glue your logic to objects that +are plumbed through to Python code. The simplest form of communication is +through **registers**: signals that are mirrored unidirectionally (either +from logic into the host, or from the host into logic). When you +instantiate registers into your design, you can access them through Python +``get`` and ``set`` functions, and the accessors behave just as any other +``asyncio``-compatible ``async`` functions would; as a result, you can +access them anywhere else in your program once your Glasgow code is running +on the hardware. + +In our previous example, we controlled Glasgow's LEDs with a pattern +generated entirely within the Glasgow hardware's programmable logic. To +demonstrate the connectivity between Glasgow hardware and host software, +we will augment it with a host-to-logic register that is wired into the +output buffers for the LEDs. + +In this example, we also demonstrate using the ``wiring.Component`` +interface to define logic. Because the previous example kept all of its +signals internal to the generated module, it had no interface to expose -- +and, as a result, you may recall that we implemented it as a raw +``Elaboratable``, rather than a ``Component``. A Component builds on +the concept of an Elaboratable, but adds a "signature" of what signals +it exports to the outside world, using properties with specially-defined +Python types. This example adds a property ``led_data`` that the ``wiring`` +subsystem will automatically fill in as a 5-bit-wide signal; the type hint +``In(5)`` suggests that the Component that we generate expects the signal to +be externally assigned. (For more information on this, `reference the +amaranth.lib.wiring documentation +`_!) +You should use the Component interface for nearly all Glasgow and Amaranth +modules that you write. + +This example also provides further documentation within its comments. To +solidify your understanding further, you may wish to consider the following +exercises: + +* What happens to the LEDs this time when you press Ctrl-C to exit the + Glasgow runtime? Why? +* Why does the "FX3" LED blink in this example, but not the previous one? +* Try using the ``add_ro_register`` API to read data back from user logic. + (For instance, you might add a second register that inverts the data + that was written to user logic, and read it back to prove that data is + correctly being written.) + +`Below, we give a program <../_static/examples/using-registers.py>`_ that +implements a similar binary counter on the LEDs, but with the computation +running on the host. You do not need any external connections other than +the Glasgow hardware itself to run this program. + +.. literalinclude:: ../_static/examples/using-registers.py + :language: python + +Glasgow should respond: + +.. code:: console + + DEBUG:asyncio:Using selector: EpollSelector + DEBUG:glasgow.hardware.device:found revC3 device with serial C3-20240518T200308Z + DEBUG:glasgow.hardware.assembly:setting port A voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:setting port B voltage to 3.30 V + DEBUG:glasgow.hardware.assembly:assigning pin tx[0] to A0 + DEBUG:glasgow.hardware.assembly:assigning pin rx[0] to B0 + DEBUG:glasgow.hardware.assembly:pulling pin B0 high + DEBUG:glasgow.hardware.toolchain:using toolchain 'builtin' (yosys 0.61.0.0.post1073, nextpnr-ice40 0.9.0.0.post686, icepack 0.9.0.0.post686) + INFO:glasgow.hardware.device:device already has bitstream ID df1012881231733da1317da8cf077ff6 + INFO:root:assembly has started diff --git a/docs/manual/src/index.rst b/docs/manual/src/index.rst index 00e8db7e3..bb506579b 100644 --- a/docs/manual/src/index.rst +++ b/docs/manual/src/index.rst @@ -12,6 +12,7 @@ Glasgow Interface Explorer manual Run in browser use/index applets/index + embedding/index library/index develop/index contribute diff --git a/docs/manual/src/use/repl-script.rst b/docs/manual/src/use/repl-script.rst index a13123c44..4e706b91b 100644 --- a/docs/manual/src/use/repl-script.rst +++ b/docs/manual/src/use/repl-script.rst @@ -84,6 +84,8 @@ If you're familiar with I²C, you'll know that a common convention is for the ta >>> await i2c_iface.read(119, 2) b'\x1c\x04' +.. _repl-uart: + UART ~~~~