Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/manual/src/_static/examples/assembly-applets.py
Original file line number Diff line number Diff line change
@@ -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())
78 changes: 78 additions & 0 deletions docs/manual/src/_static/examples/assembly-logic.py
Original file line number Diff line number Diff line change
@@ -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())
18 changes: 18 additions & 0 deletions docs/manual/src/_static/examples/assembly-skeleton.py
Original file line number Diff line number Diff line change
@@ -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())
108 changes: 108 additions & 0 deletions docs/manual/src/_static/examples/using-pins.py
Original file line number Diff line number Diff line change
@@ -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())
Loading
Loading