add bus read/write to python
This commit is contained in:
parent
f2a0ede9f4
commit
3cf5164d23
|
|
@ -9,4 +9,5 @@ Manta is a tool for debugging FPGA designs over an interface like UART or Ethern
|
||||||
|
|
||||||
Manta is written in Python, and generates Verilog-2001 HDL. It's cross-platform, and its only dependencies are pySerial and pyYAML.
|
Manta is written in Python, and generates Verilog-2001 HDL. It's cross-platform, and its only dependencies are pySerial and pyYAML.
|
||||||
|
|
||||||
For more information check out the docs at [https://fischermoseley.github.io/manta](https://fischermoseley.github.io/manta)
|
For more information check out the docs:
|
||||||
|
[https://fischermoseley.github.io/manta](https://fischermoseley.github.io/manta)
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
|
|
||||||
# How it Works
|
|
||||||
Manta works by having a set of configurable cores daisy-chained together across a simple bus that resembles AXI-lite. Each core exposes some region of addressible memory, which is accessed by the host machine over an interface of choice. Here's what this looks like as a block diagram, in this case UART is used as the interface:
|
|
||||||
|
|
||||||
## Bus
|
|
||||||
|
|
||||||
This daisy-chaining is done to make place-and-route as easy as possible - the critical timing path only exists between adjacent cores, instead of rouing back to some central core in a hub-and-spoke arrangement. This relaxed routing helps designs that span multiple clock domains and require BRAMs placed on the edges of clock domains for CDC.
|
|
||||||
|
|
||||||
## Memory
|
|
||||||
|
|
||||||
The memory is built of 16-bit registers living on a 16-bit address bus. Address space is assigned when the Verilog is generated, since each core can occupy a varying amount of address space depending on how it's configured. This space is assigned sequentially - the first core in the chain will occupy the first section of memory, and the last core will occupy the last section. Some registers are read-only to the host machine, and attempts to write to them will be ignored by the core.
|
|
||||||
|
|
||||||
## Read/Write Transactions
|
|
||||||
|
|
||||||
As you'd expect, reading from some address will elicit a response from the FGPA. However, writing to some address __will not__. If you want to verify that the data you wrote to some location is valid, read from it after the write. This is done to keep state machines simple and interfaces fast.
|
|
||||||
|
|
||||||
Data moves between the host computer and the FPGA over UART. UART's just an interface though, so the choice of what data to send is arbitrary. Manta encodes data exchanged between devices as messages, which are ASCII text in the following format:
|
|
||||||
|
|
||||||
```[preamble] [address] [data (optional)] [EOL]```
|
|
||||||
|
|
||||||
- The __preamble__ is just the character `M`, encoded as ASCII.
|
|
||||||
|
|
||||||
- The __address__ is the memory location we wish to access. This must exist somewhere in the address space consumed by the cores. If it does not, then read/write operations addressed here will do nothing. The address itself is transmitted as hex values, encoded as ASCII using the characters `0-9` and `A-F`.
|
|
||||||
|
|
||||||
- The __data__ gets stored in the memory location provided by __address__. The presence of any number of data bytes indicates a write operation, while no data bytes indicates a read operation.
|
|
||||||
|
|
||||||
- An __EOL__ indicates the end of the message. CR, LF, or both are considered valid delimiters to for messages sent to the FPGA. For messages sent to the host machine, the FPGA will send CRLF.
|
|
||||||
|
|
||||||
This message format can be either a sequence of bytes encoded over UART, or characters in a data field of an Ethernet packet.
|
|
||||||
|
|
||||||
### Example Messages
|
|
||||||
|
|
||||||
Some examples of valid messages to the FPGA are:
|
|
||||||
```MBEEF\r\n```, which writes `0xEF` to the memory at location `0xBE`.
|
|
||||||
```MBE\r\n```, which reads the value of the memory at location `0xBE`.
|
|
||||||
|
|
||||||
Some examples of invalid messages to the FPGA are:
|
|
||||||
```MBEEEF\r\n```f, which contains 12 bits of data, which isn't a multiple of 8.
|
|
||||||
```NBEEF\r\n```, which contains the wrong preamble.
|
|
||||||
|
|
||||||
For example, `M1234\r\n` specifies a read operation at address `0x1234` in the memory, and if that location contains the data `0x5678`, it will produce a response of `M5678\r\n`.
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
|
||||||
|
# How it Works
|
||||||
|
Manta works by having a set of configurable cores daisy-chained together across a simple bus that resembles AXI-lite. Each core exposes some region of addressible memory, which is accessed by the host machine over an interface of choice. Here's what this looks like as a block diagram, in this case UART is used as the interface:
|
||||||
|
|
||||||
|
## Bus
|
||||||
|
|
||||||
|
This daisy-chaining is done to make place-and-route as easy as possible - the critical timing path only exists between adjacent cores, instead of rouing back to some central core in a hub-and-spoke arrangement. This relaxed routing helps designs that span multiple clock domains and require BRAMs placed on the edges of clock domains for CDC.
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
|
||||||
|
The memory is built of 16-bit registers living on a 16-bit address bus. Address space is assigned when the Verilog is generated, since each core can occupy a varying amount of address space depending on how it's configured. This space is assigned sequentially - the first core in the chain will occupy the first section of memory, and the last core will occupy the last section. Some registers are read-only to the host machine, and attempts to write to them will be ignored by the core.
|
||||||
|
|
||||||
|
## Read/Write Transactions
|
||||||
|
|
||||||
|
As you'd expect, reading from some address will elicit a response from the FGPA. However, writing to some address __will not__. If you want to verify that the data you wrote to some location is valid, read from it after the write. This is done to keep state machines simple and interfaces fast.
|
||||||
|
|
||||||
|
Data moves between the host computer and the FPGA over UART. UART's just an interface though, so the choice of what data to send is arbitrary. Manta encodes data exchanged between devices as messages, which are ASCII text in the following format:
|
||||||
|
|
||||||
|
```[preamble] [address] [data (optional)] [EOL]```
|
||||||
|
|
||||||
|
- The __preamble__ is just the character `M`, encoded as ASCII.
|
||||||
|
|
||||||
|
- The __address__ is the memory location we wish to access. This must exist somewhere in the address space consumed by the cores. If it does not, then read/write operations addressed here will do nothing. The address itself is transmitted as hex values, encoded as ASCII using the characters `0-9` and `A-F`.
|
||||||
|
|
||||||
|
- The __data__ gets stored in the memory location provided by __address__. The presence of any number of data bytes indicates a write operation, while no data bytes indicates a read operation.
|
||||||
|
|
||||||
|
- An __EOL__ indicates the end of the message. CR, LF, or both are considered valid delimiters to for messages sent to the FPGA. For messages sent to the host machine, the FPGA will send CRLF.
|
||||||
|
|
||||||
|
This message format can be either a sequence of bytes encoded over UART, or characters in a data field of an Ethernet packet.
|
||||||
|
|
||||||
|
### Example Messages
|
||||||
|
|
||||||
|
Some examples of valid messages to the FPGA are:
|
||||||
|
```MBEEF\r\n```, which writes `0xEF` to the memory at location `0xBE`.
|
||||||
|
```MBE\r\n```, which reads the value of the memory at location `0xBE`.
|
||||||
|
|
||||||
|
Some examples of invalid messages to the FPGA are:
|
||||||
|
```MBEEEF\r\n```f, which contains 12 bits of data, which isn't a multiple of 8.
|
||||||
|
```NBEEF\r\n```, which contains the wrong preamble.
|
||||||
|
|
||||||
|
For example, `M1234\r\n` specifies a read operation at address `0x1234` in the memory, and if that location contains the data `0x5678`, it will produce a response of `M5678\r\n`.
|
||||||
|
|
||||||
|
## Python API
|
||||||
|
|
||||||
|
The Python API has two main purposes: to generate the Verilog required to instantiate debug cores on the FPGA, and to let the user easily interact with said cores. The exact Verilog and memory operations are dependent on the cores being configured and the interface between the host machine and the FPGA. This information is stored in a YAML (or JSON) configuration file, which is used to configure an instance of the `Manta` class. This maintains instances of `IOCore`, `LogicAnalyzerCore`, `LUTRAMCore`, and `BRAMCore` according to the given configuration.
|
||||||
|
|
||||||
|
### Loading configuration
|
||||||
|
|
||||||
|
Let's use the following configuration as an example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
cores:
|
||||||
|
my_io_core:
|
||||||
|
type: io
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
btnc: 1
|
||||||
|
sw: 16
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
led: 16
|
||||||
|
led16_b: 1
|
||||||
|
led16_g: 1
|
||||||
|
led16_r: 1
|
||||||
|
|
||||||
|
my_logic_analyzer:
|
||||||
|
type: logic_analyzer
|
||||||
|
sample_depth: 4096
|
||||||
|
|
||||||
|
probes:
|
||||||
|
larry: 1
|
||||||
|
curly: 1
|
||||||
|
moe: 1
|
||||||
|
shemp: 4
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
- larry && curly && ~moe
|
||||||
|
|
||||||
|
my_lut_ram:
|
||||||
|
type: lut_ram
|
||||||
|
size: 64
|
||||||
|
|
||||||
|
uart:
|
||||||
|
port: "/dev/tty.usbserial-2102926963071"
|
||||||
|
baudrate: 115200
|
||||||
|
clock_freq: 100000000
|
||||||
|
```
|
||||||
|
|
||||||
|
For each core in the config file, an instance of the corresponding Python object is added to the `Manta` object. For instance, the `Manta` instance created by the configuration above will include an `IOCore`, a `LogicAnalyzerCore`, and a `LUTRAMCore`. Each Core object is instantiated by providing the appropriate section of the config file - for instance, the logic analyzer in the config above will be created by calling `LogicAnalyzerCore(foo)`, where `foo` is:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
my_logic_analyzer:
|
||||||
|
type: logic_analyzer
|
||||||
|
sample_depth: 4096
|
||||||
|
|
||||||
|
probes:
|
||||||
|
larry: 1
|
||||||
|
curly: 1
|
||||||
|
moe: 1
|
||||||
|
shemp: 4
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
- larry && curly && ~moe
|
||||||
|
```
|
||||||
|
Stored as pythonic key-value representation. Each core also checks to make sure it's been given a sensible configuration when it is instantiated - this means the class constructors are mostly assertions about the configuration.
|
||||||
|
|
||||||
|
### Generating HDL
|
||||||
|
|
||||||
|
Once all the cores have been instantiated and stored in the `Manta` instance, Verilog can be generated. Just like how verifying each core's configuration is left up to core's corresponding Python object, generating the HDL is also left up to each core's corresponding Python object. All that's required is for each core to implement three methods:
|
||||||
|
|
||||||
|
- `hdl_inst`, which returns the module instantiation in Verilog as a python string. Any ports that need to connect to modules upstream or downstream on the bus aren't configured by the core. Those connections are made in `Manta.generate_hdl()`, which calls `Manta.generate_insts()`.
|
||||||
|
- `hdl_def`, which returns the module definition in Verilog as a Python string. This is usually either generated on-the-fly, or loaded from the Verilog source files included in the Python wheel via `pkgutil`.
|
||||||
|
- `hdl_top_level_ports`, which returns a list of any ports that the core needs tied to the top-level declaration of the `manta` module. Usually these are probes going to Logic Analyzers or IO Cores, or the TX/RX lines needed by a UART interface.
|
||||||
|
|
||||||
|
Once these have been obtained for each core, the `Manta.generate_hdl()` method will patch them all together to produce `manta.v`, which is a single file that contains all the Verilog needed to instantiate Manta. This file has the following anatomy:
|
||||||
|
|
||||||
|
- Asking each core to generate HDL instantiations, definitions, and top_level ports.
|
||||||
|
- These then get assembled into the following parts of the file:
|
||||||
|
- __Header__ - contains a little blurb about when and who generated the file
|
||||||
|
- __Top-Level Module__ - the actual definition of module manta
|
||||||
|
- __Declaration__ - contains `module manta` and top-level ports
|
||||||
|
that constitutent cores need access to
|
||||||
|
- __Interface RX__ - the modules needed to bring whatever interface the user
|
||||||
|
selected onto the bus. For UART, this is just an instance
|
||||||
|
of uart_rx and bridge_rx.
|
||||||
|
- __Core Chain__ - the chain of cores specified by the user. This follows
|
||||||
|
a sequence of:
|
||||||
|
- Core Instance - HDL specifying an instance of the core.
|
||||||
|
- Core Connection - HDL specifying the registers that connect one
|
||||||
|
core to the next.
|
||||||
|
- Core Instance
|
||||||
|
- Core Connection
|
||||||
|
....
|
||||||
|
|
||||||
|
This repeats for however many cores the user specified.
|
||||||
|
|
||||||
|
- __Interface TX__ - the modules needed to bring the bus out to whatever
|
||||||
|
interface the user selected. For UART, this is just
|
||||||
|
an instance of bridge_tx and uart_tx.
|
||||||
|
- __Footer__ - just the 'endmodule' keyword.
|
||||||
|
|
||||||
|
- __Module Definitions__ - all the source for the modules instantiated in the
|
||||||
|
top-level module.
|
||||||
|
|
||||||
|
|
||||||
|
### Using Cores
|
||||||
|
|
||||||
|
Once manta's been generated, included in your project, and built, the Python API provides methods for interfacing with the cores.
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
## IO Core
|
## IO Core
|
||||||
- add interface read/write to python
|
|
||||||
- test examples that build
|
- test examples that build
|
||||||
- update IO core read/write memory handling to be less ugly
|
- update IO core read/write memory handling to be less ugly
|
||||||
- add logic for ports >16 bits in width
|
- add logic for ports >16 bits in width
|
||||||
|
|
@ -28,12 +27,6 @@
|
||||||
- if we see a FT2232 on the device we should grab it
|
- if we see a FT2232 on the device we should grab it
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
- Move stuff out of readme.md and into the docs front page - right now information is duplicated
|
|
||||||
- Write out what technologies are being used here (iverilog for sim, gtkwave for vcd, makefile for simulation/lint/pushing to pypi, github actions for automated test and building the doc site, python for autogeneration, verilator for lint. for docs, mkdoc-material for the site, wavedrom for timing diagrams and draw.io for block diagrams)
|
|
||||||
- Write out where stuff is being stored - `test/` contains all the systemverilog testbenches, `src/manta` contains all verilog and python needed to generate and run the cores, `doc/` contains the documentation site source files, `.github/` contains the github actions config, `examples/` is exactly what it sounds like
|
|
||||||
|
|
||||||
- Write out the anatomy of manta.v and how `Manta` generates that
|
|
||||||
- Write out what methods need to be implemented for Manta.generate_hdl() to actually be able to pick up on the cores
|
|
||||||
- Write out what bus transactions look like and how messages get passed. probably going to need wavedrom for this.
|
- Write out what bus transactions look like and how messages get passed. probably going to need wavedrom for this.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
## What Goes Where?
|
||||||
|
|
||||||
|
- `src/manta/` contains the Python and Verilog source needed to generate and run the cores.
|
||||||
|
- `test/` contains testbenchs for HDL. Manta is written in Verilog 2001, but the testbenches are written in SystemVerilog 2012. These are simulated using Icarus Verilog, which produces `.vcd` files, viewable your favorite waveform viewer, like GTKWave.
|
||||||
|
- `doc/` contains the documentation you're reading right now! It's built into a nice static site by Material for MkDocs, which automatically rebuilds the site on every commit to `main`. This is done with a GitHub Action configured in `.github/`
|
||||||
|
- `examples/` is exactly what it sounds like. It contains examples for both the Digilent Nexys 4 DDR/Nexys A7 with thier onboard Series-7, as well as the Icestick with its onboard iCE40.
|
||||||
|
- `.github/` also contains some GitHub Actions configuration for automatically running the SystemVerilog testbenches and building the examples, in addition to automatically rebuilding the site.
|
||||||
|
|
||||||
|
## Tools Used
|
||||||
|
- Verilator is used for linting
|
||||||
|
- Wavedrom for waveform diagrams, and draw.io for block diagrams
|
||||||
|
- GNU make for making it more convenient to run simulations.
|
||||||
|
- GitHub Pages serves the documentation site.
|
||||||
15
mkdocs.yml
15
mkdocs.yml
|
|
@ -47,9 +47,12 @@ markdown_extensions:
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- User Reference:
|
||||||
- Getting Started: getting_started.md
|
- Home: index.md
|
||||||
- How it Works: how_it_works.md
|
- Getting Started: getting_started.md
|
||||||
- Logic Analyzer Core: logic_analyzer_core.md
|
- Logic Analyzer Core: logic_analyzer_core.md
|
||||||
- IO Core: io_core.md
|
- IO Core: io_core.md
|
||||||
- To Do: todo.md
|
- Developer Reference:
|
||||||
|
- System Architecture: system_architecture.md
|
||||||
|
- Tools Used: tools_used.md
|
||||||
|
- To Do: todo.md
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,28 @@ class UARTInterface:
|
||||||
|
|
||||||
def write(self, bytes):
|
def write(self, bytes):
|
||||||
self.ser.write(bytes)
|
self.ser.write(bytes)
|
||||||
|
|
||||||
|
def read_register(self, addr):
|
||||||
|
# request from the bus
|
||||||
|
addr_hex = hex(addr).split("0x")[-1] # TODO: turn this into format()
|
||||||
|
request = f"M{addr_hex}\r\n".encode('ascii')
|
||||||
|
self.write(request)
|
||||||
|
|
||||||
|
# read and parse the response
|
||||||
|
response = self.read(7)
|
||||||
|
assert response[0] == 'M'.encode('ascii'), "Bad message recieved, incorrect preamble."
|
||||||
|
assert response[-1] == '\n'.encode('ascii'), "Bad message received, incorrect EOL."
|
||||||
|
assert response[-2] == '\r'.encode('ascii'), "Bad message received, incorrect EOL."
|
||||||
|
assert len(response) == 7, f"Wrong number of bytes received, expecting 7 but got {len(response)}."
|
||||||
|
|
||||||
|
return int(response[1:4].decode('ascii'))
|
||||||
|
|
||||||
|
def write_register(self, addr, data):
|
||||||
|
addr_hex = hex(addr).split("0x")[-1] # TODO: turn this into format()
|
||||||
|
data_hex = hex(data).split("0x")[-1]
|
||||||
|
|
||||||
|
msg = f"M{addr_hex}{data_hex}\r\n".encode('ascii')
|
||||||
|
self.write(msg)
|
||||||
|
|
||||||
def hdl_top_level_ports(self):
|
def hdl_top_level_ports(self):
|
||||||
# this should return the probes that we want to connect to top-level, but like as a string of verilog
|
# this should return the probes that we want to connect to top-level, but like as a string of verilog
|
||||||
|
|
@ -139,6 +161,48 @@ class IOCore:
|
||||||
self.max_rel_addr = address
|
self.max_rel_addr = address
|
||||||
address += 1
|
address += 1
|
||||||
|
|
||||||
|
def set(self, probe, data):
|
||||||
|
# check that probe actually exists
|
||||||
|
assert (probe in self.inputs) or (probe in self.outputs), "Probe {probe} not found."
|
||||||
|
|
||||||
|
if probe in self.inputs:
|
||||||
|
probe_def = self.inputs[probe]
|
||||||
|
|
||||||
|
elif probe in self.outputs:
|
||||||
|
probe_def = self.outputs[probe]
|
||||||
|
|
||||||
|
|
||||||
|
# check that value is reasonable
|
||||||
|
# (should be an integer between 0 and 2^width - 1)
|
||||||
|
|
||||||
|
# send message
|
||||||
|
addr = probe_def["address"] + self.base_addr
|
||||||
|
|
||||||
|
|
||||||
|
assert isinstance(data, int), "Data must be an integer."
|
||||||
|
if data > 0:
|
||||||
|
assert data <= (2**probe_def["width"]) - 1, f"Unsigned value too large for probe of width {probe_def['width']}"
|
||||||
|
|
||||||
|
elif data < 0:
|
||||||
|
assert data >= -(2**(probe_def["width"]-1))-1, f"Signed value too large for probe of width {probe_def['width']}"
|
||||||
|
assert data <= (2**(probe_def["width"]-1))-1, f"Signed value too large for probe of width {probe_def['width']}"
|
||||||
|
|
||||||
|
self.interface.write_register(addr, data)
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, probe):
|
||||||
|
# check that probe actually exists
|
||||||
|
assert (probe in self.inputs) or (probe in self.outputs), "Probe {probe} not found."
|
||||||
|
|
||||||
|
if probe in self.inputs:
|
||||||
|
probe_def = self.inputs[probe]
|
||||||
|
|
||||||
|
elif probe in self.outputs:
|
||||||
|
probe_def = self.outputs[probe]
|
||||||
|
|
||||||
|
addr = self.base_addr + probe_def["address"]
|
||||||
|
return self.interface.read_register(addr)
|
||||||
|
|
||||||
def hdl_inst(self):
|
def hdl_inst(self):
|
||||||
inst_ports = ""
|
inst_ports = ""
|
||||||
for input in self.inputs:
|
for input in self.inputs:
|
||||||
|
|
@ -739,35 +803,6 @@ module manta (
|
||||||
|
|
||||||
|
|
||||||
def generate_hdl(self, output_filepath):
|
def generate_hdl(self, output_filepath):
|
||||||
"""
|
|
||||||
This function generates manta.v, which has the following anatomy:
|
|
||||||
- Header - contains a little blurb about when and who generated the file
|
|
||||||
- Top-Level Module - the actual definition of module manta
|
|
||||||
- Declaration - contains `module manta` and top-level ports
|
|
||||||
that constitutent cores need access to
|
|
||||||
- Interface RX - the modules needed to bring whatever interface the user
|
|
||||||
selected onto the bus. For UART, this is just an instance
|
|
||||||
of uart_rx and bridge_rx.
|
|
||||||
- Core Chain - the chain of cores specified by the user. This follows
|
|
||||||
a sequence of:
|
|
||||||
- Core Instance - HDL specifying an instance of the core.
|
|
||||||
- Core Connection - HDL specifying the registers that connect one
|
|
||||||
core to the next.
|
|
||||||
- Core Instance
|
|
||||||
- Core Connection
|
|
||||||
....
|
|
||||||
|
|
||||||
This repeats for however many cores the user specified.
|
|
||||||
|
|
||||||
- Interface TX - the modules needed to bring the bus out to whatever
|
|
||||||
interface the user selected. For UART, this is just
|
|
||||||
an instance of bridge_tx and uart_tx.
|
|
||||||
- Footer - just the 'endmodule' keyword.
|
|
||||||
|
|
||||||
- Module Definitions - all the source for the modules instantiated in the
|
|
||||||
top-level module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# generate header
|
# generate header
|
||||||
header = self.generate_header()
|
header = self.generate_header()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue