OpenVizsla OV3 - Hello, World!
I’ve previously talked about the hardware design of the the OpenVizsla OV3 USB hardware analyzer, and about the FPGA design.
Now we could use OV3 to sniff some USB traffic - but we can do this later as well. Instead we should write a ‘hello world’-type component.
Hello, World
I’ve chosen to implement something that allows to write an 8-bit register, does a computation (invert all bits), and allow that to be read back. Sounds simple? It is!
What Migen does for you
TL;DR: Migen doesn’t replace your brain.
One warning - Migen allows you to generate FPGAs design using Python. It does not allow you to write “Python code on your FPGA”, and it doesn’t attempt to provide “a subset of Python that can be synthesized”. This is not how Migen works.
You’re still assembling logic. Migen helps
you in describing this logic. Have you drawn schematics before? Think of a writing a Python program that
generates a schematic based on your ideas. Python allows you to script all parts of this
process, so instead of drawing 20 NAND-gates, you can write a for
-loop that
draws these 20 NAND-gates for you. Or you can make function that draws a
full-blown flipflop at a given location on the sheet. Or something that
takes care of all the boring wires like clock and reset. So you can use all
fancy Python features as much as you want, but the end result will still be
a schematic diagram.
Migen is very similar, except that it creates verilog in the end - just a different representation of a schematic diagram.
The Business Logic
Anyway, let’s go ahead. We need to write the business end of our tool, which
XORs the bits. We start by writing a Module
which contains two CSRs. CSRs
are registers that can be accessed from the host. They can be either read-only from
host (CSRStatus
), read-only from FPGA (CSRStorage
), or more complicated,
user-defined (CSR
). We use a CSRStorage
to implement an input port, and CSRStatus
to deliver the result of the very complicated XOR computation.
We create a file called software/fpga/ovhw/hello.py
with the
following content:
from migen.fhdl.std import *
from migen.bank import description
class Hello(Module, description.AutoCSR):
def __init__(self):
self._input = description.CSRStorage(8)
self._output = description.CSRStatus(8)
self.sync += self._output.status.eq(self._input.storage ^ 0xff)
The first line gets us the basic Migen primitives like Module
. We define a
class Hello
, which is a Migen Module
, that we will later insert into the
OV3 design.
The second line gets us the AutoCSR
feature, which we use to create
CSR registers. In the background, the Migen feature creates a list of all
registers in the design, and ties them to the CSR BankArray
, similar to
BTN_status
, LED_outputs
etc.
We define two CSR registers, _input
and _output
. _input
is readable
from our design, and _output
is writable. Both are 8 bit in size.
Finally, we do the computation - self.sync
is a list of everything that
happens at a clock edge. (More accurately, on a clock edge in the sys
clock domain. There can be multiple, but we simply run in the same clock
domain as the rest of the CSR logic, which simplifies things. On OV3, this
was chosen to be a 100MHz clock.)
Wiring it Up
Now we need to add our Module to the OV3 design. The top-level module is
defiend in software/fpga/ovhw/top.py
:
diff --git a/software/fpga/ov3/ovhw/top.py b/software/fpga/ov3/ovhw/top.py
index 5813e8f..0cd9a9a 100644
--- a/software/fpga/ov3/ovhw/top.py
+++ b/software/fpga/ov3/ovhw/top.py
@@ -23,6 +23,7 @@ from ovhw.ftdi_lfsr_test import FTDI_randtest
from ovhw.ulpicfg import ULPICfg
from ovhw.cfilt import RXCmdFilter
from ovhw.ov_types import ULPI_DATA_D
+from ovhw.hello import Hello
import ovplatform.sdram_params
class OV3(Module):
@@ -115,6 +116,8 @@ class OV3(Module):
self.submodules.buttons = BTN_status(~plat.request('btn'))
+ # hello, world
+ self.submodules.hello = Hello()
# Bind all device CSRs
self.csr_map = {
@@ -124,6 +127,7 @@ class OV3(Module):
'randtest' : 3,
'cstream' : 4,
'sdram_test' : 5,
+ 'hello' : 6,
}
self.submodules.csrbankarray = BankArray(self,
3 simple changes: We import our new module that we created above, we add it
as a submodule
to the OV3
-top-level module, and we assign a register
prefix to the registers in the hello
CSR space. Don’t worry about the
actual number, as it automagically ends up in the map file, which is later
consumed by ovctl.py
.
Synthesize the thing by running make
in software/fpga/ovhw
, and you
should end up with a new firmware package, in
software/fpga/ovhw/build/build/ov3.fwpkg
. A firmware package contains an
FPGA bitstream and the register mapping files.
Accessing the Logic
The map file can also be found in
software/fpga/ovhw/build/build/map.txt
and describes the address of all
CSR resources. For our hello
component, the following lines were
automatically added by the build process:
# hello
HELLO_BASE = 0xc00
HELLO_INPUT = 0xc00
HELLO_OUTPUT = 0xc01
It means that the _input
register is at address 0xC00, and the _output
register is at address 0xC01. Again, no need to remember the values, as the
map file gets parsed automatically.
Finally we need to add code to talk to our new module in
software/host/ovctl.py
:
@command('hello', ('v', int16))
def hello(dev, v):
dev.regs.hello_input.wr(v)
print("Hello returns %02x" % dev.regs.hello_output.rd())
@command
registers the following function to the command line handler. We
expect a single argument, an integer.
Our implementation of that command simply writes that value to the
hello
-Module _input
-register, then reads back then value from the
_output
register, and displays it.
Finally, we instruct ovctl.py to take the new fwpkg (--pkg
../fpga/ov3/build/ov3.fwpkg
), we force-reload the bitstream (-l
), and
invoke our hello
function:
$ python3 ovctl.py -l --pkg ../fpga/ov3/build/ov3.fwpkg hello 12
FPGA: Bitstream timestamp 2014/08/27 13:35:19
FPGA: sending configuration bitstream
FPGA: CRC OK
FPGA: configured
Hello returns ed
We quickly compute that 0x12 is really the inversion of 0xED, and call this success.
You can find this code on github.