Getting Started

Now that you’ve [installed] Torii ILA, you’re all ready to start using it, or if you rather just want something to copy paste, you can find that in the TL;DR at the end.

This section of the docs will walk you through a very simple initial setup by using one of the examples that are provided.

This guide will cover the USB ILA example bitsy_usb_ila.py, but we’ll have the small changes in the UART example at the end.

Example Prerequisites

There are a few extra prerequisites needed, in this case you need a iCEBreaker Bitsy from 1BitSquared and to have Torii Boards installed.

It is also expected you have a VCD viewer installed, such as Surfer or gtkwave.

Example: USB Integrated Logic Analyzer

Now, with all of the prerequisites sorted, lets guide you through the example and how to set up a USB based Torii ILA.

Gateware

First up is the actual gateware that will be run on the device, it’s the example ILA along with some example signals and support bits.

PLL Module

The first big chunk is the PLL elaboratable, this is platform specific, but it shows how to set up an ICE40UP5K PLL for use as a USB device with Torii-USB.

42class PLL(Elaboratable):
43	''' 12MHz -> 48MHz PLL '''
44	def __init__(self) -> None:
45		self.locked = Signal()
46
47	def elaborate(self, platform: Platform) -> Module:
48		m = Module()
49
50		m.domains.sync    = ClockDomain()
51		m.domains.usb     = ClockDomain()
52		m.domains.usb_io  = ClockDomain()
53
54		platform.lookup(platform.default_clk).attrs['GLOBAL'] = False
55
56		pll_clk = Signal()
57		usb_clk = Signal()
58
59		m.submodules.pll = Instance(
60			'SB_PLL40_PAD',
61			i_PACKAGEPIN    = platform.request(platform.default_clk, dir = 'i'),
62			i_RESETB        = Const(1),
63			i_BYPASS        = Const(0),
64
65			o_PLLOUTCORE    = pll_clk,
66			o_LOCK          = self.locked,
67
68			p_FEEDBACK_PATH = 'SIMPLE',
69			p_PLLOUT_SELECT = 'GENCLK',
70			p_DIVR          = 0,
71			p_DIVF          = 63,
72			p_DIVQ          = 4,
73			p_FILTER_RANGE  = 1
74		)
75
76		platform.add_clock_constraint(pll_clk, 48e6)
77		platform.add_clock_constraint(usb_clk, 12e6)
78
79		clk_div = Signal(range(4))
80		m.d.usb_io += [ clk_div.inc(), ]
81
82		m.d.comb += [
83			usb_clk.eq(clk_div[1]),
84
85			ClockSignal('sync').eq(pll_clk),
86			ClockSignal('usb_io').eq(pll_clk),
87			ClockSignal('usb').eq(usb_clk),
88
89			ResetSignal('sync').eq(~self.locked),
90			ResetSignal('usb_io').eq(~self.locked),
91			ResetSignal('usb').eq(~self.locked),
92		]
93
94		return m

The big thing to note here, as we will need it later, is the resulting frequency of the sync domain, which in this case is 48e6Hz or 48MHz.

Top Module

Next up is the main elaboratable module we will be synthesizing, in this case called Top. We set up a few example signals for us to capture on the FPGA so we can be sure that the ILA is doing something.

102class Top(Elaboratable):
103	def __init__(self) -> None:
104		counter_val = int(48 // 10)
105		# Handful of sample signals for the ILA
106		self.pll_locked = Signal()
107		self.timer      = Signal(range(counter_val), reset = counter_val - 1, decoder = EnumValue)
108		self.flops      = Signal(range(8), reset = 1)
109		self.other      = Signal(8)

The timer signal has an enum based decoder attached to it, this helps us show that the resulting VCD from the ILA retains signal decoders and emits them properly in the trace, it looks as follows:

 96class EnumValue(Enum):
 97	Foo = 0
 98	Bar = 1
 99	Baz = 2
100	Qux = 3

Last thing in the constructor is the actual creation of the USBIntegratedLogicAnalyzer itself, we give it all the signals we want to keep track of, how deep we want our sample memory, as well as the sample rate which we got from the PLL config and the USB resource name for it to use.

112		self.ila = USBIntegratedLogicAnalyzer(
113			# The initial set of signals we care about
114			signals = [
115				self.pll_locked,
116				self.timer,
117				self.flops,
118				self.other,
119			],
120			# How many samples we want to capture
121			sample_depth = 32,
122			# How fast our sample domain is, in this case `sync`
123			sample_rate  = 48e6,
124			# The name of the USB resource to pull from the platform.
125			bus = 'usb',
126		)

Now we move on to the elaborate method, most of the stuff in here is just to support the example, but here we also ensure the ILA is a submodule, and also show an example of adding “private” signals to the ILA for capture.

Adding the ILA as a submodule is typical to torii:

139		m.submodules.ila = self.ila

The important thing in here is just below that, it’s where we create two module internal signals, and add them to the ILA for capture.

141		wiggle = Signal()
142		woggle = Signal()
143		trig  = Signal()
144
145		# Add some "Private" signals to the ILA
146		self.ila.append_signals([wiggle, woggle])

Along with append_signals there is also an add_signal that lets you add a single Signal at a time rather than an iterable of them.

The remainder of the logic in elaborate is simply just driving the example signals:

148		with m.FSM(name = 'meow') as f:
149			self.ila.add_fsm(f)
150
151			with m.State('IDLE'):
152				with m.If(self.flops[1]):
153					m.next = 'WIGGLE'
154
155			with m.State('WIGGLE'):
156				m.d.sync += [
157					wiggle.eq(self.timer[0]),
158					woggle.eq(~wiggle),
159				]
160
161				with m.If(self.flops[2]):
162					m.next = 'IDLE'
163
164		# Dummy logic wiggles
165		with m.If(self.timer == 0):
166			m.d.sync += [
167				self.timer.eq(self.timer.reset),
168				self.flops.eq(self.flops.rotate_left(1)),
169			]
170			with m.If(self.flops[2]):
171				m.d.sync += [ self.other.inc(), ]
172
173		with m.Else():
174			m.d.sync += [ self.timer.eq(self.timer - 1), ]

Building and Backhaul

Now that we have been aquatinted with the gateware, we can move on to the more exciting bits, actually building it, loading it onto a device, and getting real ILA samples back.

Platform Setup and Building

The first handful of lines after main() are just setup, we create an instance of our Top module, set the VCD destination file name, and create the platform for the iCEBreaker Bitsy that we are targeting.

192	top      = Top()
193	vcd_file = Path.cwd() / 'bitsy_usb_ila.vcd'
194	plat     = ICEBreakerBitsyPlatform()

After that is the actual gateware synthesis and upload, we simply call plat.build with a handful of arguments to get everything handled for us.

197	try:
198		plat.build(
199			top, name = 'bitsy_usb_ila', do_program = True,
200			script_after_read = 'scratchpad -copy abc9.script.flow3 abc9.script\n',
201			synth_opts = ['-abc9'],
202			nextpnr_opts = [ '--seed 1' ]
203		)
204	except CalledProcessError as e:
205		# dfu-util complains because we don't come back as a DFU device
206		# In that case we don't care there was an error
207		if e.returncode != 251:
208			raise e

Some of these options need explaining, the name, and do_program are self explanatory, but the last three are a bit more archaic.

The script_after_read option is a string that is a Yosys command that is added to the synthesis script right after the design source is read in, this is prior to synthesis actually being executed. What this line is doing, is making abc9 run twice, to try to better optimize the design. This is used in conjunction with the synth_opts option of -abc9, which tells Yosys when synthesizing the design to use abc9.

The nextpnr_opts is simply passing a PNR seed that was found to work for this design, depending on nextpnr internal changes, the phase of the moon, or how grumpy your cat is that day, you might need to adjust this to make sure timings pass.

This whole thing is wrapped in a try/except because the USBIntegratedLogicAnalyzer does not set up a fully DFU compliant device, so when dfu-util sees the device come back and it is missing the DFU endpoint it gets cranky, but that’s okay.

Backhaul and Data Exfiltration

Now that all the gateware specific details have been dealt with, we can get data off the device! Exciting!

The first little bit just prints out the ILA configuration information for us, it’s not too important, the main focus here is getting our hands on a USBIntegratedLogicAnalyzerBackhaul interface.

That is a lot more straightforward than it seems:

217	backhaul = top.ila.get_backhaul()

See? It’s that easy!

You can actually do it two ways, the first being getting it from the ILA instance from the gateware, that is what is done above, and it has the advantage of just giving you a fully set up backhaul interface.

The other way is to manually construct a USBIntegratedLogicAnalyzerBackhaul and pass the ILA instance into it, so it can read the information it needs.

In the case of the USB ILA, the VID/PID are fixed, and the backhaul interface looks for them by default.

Next, we dump the decoded samples to stdout, this has a whole lot of machinery working behind the scenes to give us a simple signal name:value dictionary out from the USB device.

219	for ts, sample in backhaul.enumerate():
220		print(f'{ts / 1e-9:.2f} ns:')
221		for name, val in sample.items():
222			print(f'  {name}: {val}')

After we list out the collected samples to stdout, we then write a VCD file:

224	backhaul.write_vcd(vcd_file)

Running the Example

Now we’re ready to run!

Hold down the button on the bitsy next to the USB port while you plug it in, this will cause it to boot into bootloader mode. This will be indicated by a flashing blue on the RGB LED.

Running lsusb -d 1d50:6146 should show the OpenMoko, Inc. iCEBreaker bitsy v1.x (DFU) device.

Next, simply run python bitsy_usb_ila.py, you should see it say Building gateware... followed by a bit of a delay. After that dfu-util should show that it is programming the device.

After another short delay of the device coming back, you should see the following output:

ILA Info:
  bytes per sample: 2
  bits per sample:  16
  sample rate:      48.0 MHz
  sample period:    20.833333333333332 ns
Collecting ILA Samples
0.00 ns:
  pll_locked: 1
  timer: 11
  flops: 001
  other: 10000000
  wiggle: 0
  woggle: 0

-- SNIP --

Writing to VCD: /path/to/bitsy_usb_ila.vcd

With that you should now have a VCD to load and look at, feel free to poke around, but it should look something like the following:

A screenshot of Surfer showing all the signals hooked up to the ILA

Note

Hot tip, you can use the web version of Surfer at https://app.surfer-project.org/ to load the VCD in your web browser

You may have noticed the ila_clk signal in the list and wonder where it came from. It’s a synthetic signal that is generated using the sample rate you gave the ILA. It is effectively the sample clock used in the ILA, but is not a direct capture of it.

Conclusion and UART Notes

And with that, you’re done! You’re all ready to go and use Torii ILA in your projects.

The UART ILA is much the same, the interface for everything is basically identical except in two spots. The first being in the actual construction of the ILA module itself:

111		self.ila = UARTIntegratedLogicAnalyzer(
112			# UART Divisor (clk // baud)
113			divisor = int(48e6 // SERIAL_PORT_BAUD),
114			# UART IO
115			tx = self.tx, rx = self.rx,

It has 3 extra kwargs, the first being divisor, this is the divisor used to drive the UART to reach the requested baud on the output clock domain, it should be int(clk_speed // baud). The next two are tx and rx, these are the signals that should be tied to the boards UART transmit and receive pins.

The second difference is in the backhaul interface:

231	# Set up the serial port we are going to use to ingest the data
232	serialport = Serial(port = SERIAL_PORT_PATH, baudrate = SERIAL_PORT_BAUD)
233	# Get the backhaul interface from the ILA module
234	backhaul = top.ila.get_backhaul(serialport)

The UART backhaul specifically wants a pre-configured pyserial Serial object due to the nature of the interface.

Other than that, the rest of the API is identical.

TL;DR

If you really just need something to drop in,

 1     # In your imports section
 2     from torii_ila import USBIntegratedLogicAnalyzer
 3
 4     # ...
 5
 6     # In your Torii Elaboratable `__init__`
 7     self.ila = USBIntegratedLogicAnalyzer(
 8             # The initial set of signals we care about
 9             signals = [
10                     # List of Signals
11             ],
12             # How many samples we want to capture
13             sample_depth = 32,
14             # How fast our sample domain is, in this case `sync`
15             sample_rate  = 48e6,
16             # The name of the USB resource to pull from the platform.
17             bus = 'usb',
18     )
19
20     # ...
21
22     # In the def elaborate() method
23
24     m.submodules.ila = self.ila
25
26     # Wherever you need to get the backhaul interface
27
28     backhaul = top.ila.get_backhaul()
29     backhaul.write_vcd(vcd_file)
 1     # In your imports section
 2     from torii_ila import UARTIntegratedLogicAnalyzer
 3
 4     # ...
 5
 6     # In your Torii Elaboratable `__init__`
 7     self.ila = UARTIntegratedLogicAnalyzer(
 8             # UART Divisor (clk // baud)
 9             divisor = int(48e6 // SERIAL_PORT_BAUD),
10             # UART IO
11             tx = self.tx, rx = self.rx,
12             # The initial set of signals we care about
13             signals = [
14                     # List of Signals
15             ],
16             # How many samples we want to capture
17             sample_depth = 32,
18             # How fast our sample domain is, in this case `sync`
19             sample_rate  = 48e6
20     )
21
22     # ...
23
24     # In the def elaborate() method
25
26     m.submodules.ila = self.ila
27
28     # Wherever you need to get the backhaul interface
29     backhaul = top.ila.get_backhaul(serialport)
30     backhaul.write_vcd(vcd_file)