docs: update memory_core

This commit is contained in:
Fischer Moseley 2024-09-03 21:54:19 -06:00
parent 2c124200da
commit b4ef5502c1
5 changed files with 97 additions and 58 deletions

View File

@ -38,8 +38,8 @@ Inside this configuration, the following parameters may be set:
- `name` _(required)_: The name of the IO core. This name is used to reference the core when working with the API, and can be whatever you'd like.
- `type` _(required)_: This denotes that this is an IO core. All cores contain a `type` field, which must be set to `io` to be recognized as an IO core.
- `inputs` _(optional)_: This lists all inputs from from the FPGA fabric to the host machine. Signals in this list may be read by the host, but ___cannot___ be written to. Specifying input probes is optional - it's perfectly valid to have an IO core with only output probes.
- `outputs` _(optional)_: This lists all outputs from the host machine to the FPGA fabric. Signals in this list are usually written to by the host, but they can also be read from. Doing so returns the value last written to the register. Specifying output probes is optional - it's perfectly valid to have an IO core with only input probes.
- `inputs` _(optional)_: This lists all inputs from from the FPGA fabric to the host machine. Signals in this list may be read by the host, but ___cannot___ be written to. This parameter is somewhat optional as an IO Core must have at least one probe, but it need not be an input.
- `outputs` _(optional)_: This lists all outputs from the host machine to the FPGA fabric. Signals in this list are usually written to by the host, but they can also be read from. Doing so returns the value last written to the register. This parameter is somewhat optional as an IO Core must have at least one probe, but it need not be an output.
- `initial_value` _(optional)_: This sets an initial value for an output probe to take after the FPGA powers on. This is done with an `initial` statement in Manta's Verilog, and is independent of the input clock or resets elsewhere in the FPGA. This parameter is optional, and defaults to zero.
<!-- - `user_clock` _(optional)_: If set to True, an extra input port will be added to the `manta` module for an clock input to run the IO core on. This lets the IO Core handle clock domain crossing through its internal buffers. If set to False, Manta will run the IO core from its internal clock (the one provided through `manta`'s `clk` port). This parameter is optional, and defaults to False. More information is available in the [architecture](../architecture#io-core) page. -->

View File

@ -1,12 +1,28 @@
## Overview
Memory is the de facto means of storing data on FPGAs when the space needed exceeds a few registers. As a result, Manta provides a Memory core, which instantiates a dual-port RAM on the FPGA. One port is provided to the host, and the other is connected to your logic with the standard RAM interface (`addr`, `data_in`, `data_out`, `write_enable`). This allows the host to provide reasonably large amounts of data to user logic - or the other way around, or a mix of both!
This is a very, very simple task - and while configuration is straightforward, there are a few caveats. More on both topics below:
Memory is used to store data when the space needed exceeds a few registers. As a result, Manta provides a Memory core, which instantiates a dual-port RAM on the FPGA. One port is provided to the host, and the other is connected to your logic with a simple `addr`/`data_in`/`data_out`/`write_enable` interface. This allows the host machine to exchange larger amounts of data with your logic on the FPGA.
This is a very, very simple task - however it's surprisingly useful in practice. Both the [Use Cases](../use_cases) page and the repository's [examples](https://github.com/fischermoseley/manta/tree/main/examples) folder contain examples of the Memory Core for your reference.
Manta won't impose any limit on the width or depth of the memory you instantiate, but you will be limited by the available resources and timing properties of your FPGA.
!!! warning "Words update 16 bits at a time!"
Due to the structure of Manta's internal bus, the Memory core only updates 16 bits of a word at a time. For instance, writing a new value to a 33-bit wide memory would update bits 0-15 on one clock cycle, bits 16-31 on another, and bit 32 on another still. Manta makes no guaruntees about the time taken between each of these updates. If this is a problem for your application, consider using an IO Core as a doorbell to signal when the memory is valid, or ping-pong between two Memory Cores.
## On-Chip Implementation
Manta will make a best-effort attempt to implement the memory in Block RAM, if it is available on the device. This is done by exporting Verilog that synthesis tools should infer as Block RAMs, however this inference is not guarunteed. Depending on your toolchain and the FPGA's architecture, the Verilog produced by Manta may be implemented as FF RAM, LUT (Distributed) RAM, or something else. These memory types are well explained in the [Yosys documentation](https://yosyshq.readthedocs.io/projects/yosys/en/latest/using_yosys/synthesis/memory.html), but be sure to check your toolchain's documentation as well.
## Configuration
Just like the rest of the cores, the Memory core is configured via an entry in a project's configuration file. This is easiest to show by example:
As explained in the [getting started](../getting_started) page, the Memory Core must be configured and included in the FPGA design before it can be operated. Configuration is performed differently depending on if you're using a traditional Verilog-based workflow, or if you're building an Amaranth-native design.
### Verilog-Based Workflows
Cores are configured with an entry in a project's configuration file when using a Verilog-based workflow, and the Memory Core is no different. This is best shown by example:
```yaml
---
@ -19,7 +35,7 @@ cores:
```
There's a few parameters that get configured here, including:
Inside this configuration, the following parameters may be set:
- `name`: The name of the Memory core. This name is used to reference the core when working with the API, and can be whatever you'd like.
- `type`: This denotes that this is a Memory core. All cores contain a `type` field, which must be set to `memory` to be recognized as an Memory core.
@ -27,45 +43,18 @@ There's a few parameters that get configured here, including:
- `width`: The width of the Memory core, in bits.
- `depth`: The depth of the Memory core, in entries.
Manta won't impose any limit on the width or depth of the memory you instantiate, but since Manta instantiates BRAM primitives on the FPGA, you will be limited by what your FPGA can support. It helps to know your particular FPGA's architecture here.
### Amaranth-Native Designs
### On-Chip Implementation
Since Amaranth modules are Python objects, the configuration of the Memory Core is given by the arguments given during initialization. See the documentation for the `MemoryCore` [class constructor](#manta.MemoryCore) below, as well as the Amaranth [examples](https://github.com/fischermoseley/manta/tree/main/examples/amaranth) in the repo.
For most use cases, Manta will choose to implement the memory in Block RAM, if it is available on the device. However, the Verilog produced by Manta may be inferred to a number of memory types, including FF RAM or LUT (Distributed) RAM. For more information on how this is chosen, please refer to the [Yosys documentation](https://yosyshq.readthedocs.io/projects/yosys/en/latest/CHAPTER_Memorymap.html).
### Python API
## Operation
The Memory core functionality is stored in the `Manta.MemoryCore` classes in [src/manta/memory_core.py](https://github.com/fischermoseley/manta/blob/main/src/manta/memory_core.py), and it may be controlled with the two functions:
Regardless of the technique you used to configure your Memory Core, it is operated using the [`read()`](#manta.MemoryCore.read) and [`write()`](#manta.MemoryCore.write) methods. Documentation for these methods is available below.
Just like with the other cores, interfacing with the Memory with the Python API is simple:
These methods are members of the `MemoryCore` class, so if you're using Manta in a Verilog-based workflow, you'll first need to obtain a `Manta` object that contains an `MemoryCore` member. This is done with `Manta.from_config()`, as shown in the Verilog [examples](https://github.com/fischermoseley/manta/tree/main/examples/verilog).
```python
from manta import Manta
m = manta('manta.yaml')
m.my_memory.write(addr=38, data=600)
m.my_memory.write(addr=0x1234, data = 0b100011101011)
m.my_memory.write(0x0612, 0x2001)
## Python API Documentation
foo = m.my_memory.write(addr=38)
foo = m.my_memory.write(addr=1234)
foo = m.my_memory.write(0x0612)
```
Reading/writing in batches is also supported. This is recommended where possible, as reads are massively sped up by performing them in bulk:
```python
addrs = list(range(0, 1234))
datas = list(range(1234, 2468))
m.my_memory.write(addrs, datas)
foo = m.my_memory.read(addrs)
```
### Synchronicity
Since Manta's [data bus](architecture.md#data-bus) is only 16-bits wide, it's only possible to manipulate the Memory core in 16-bit increments. This means that if you have a RAM that's ≤16 bits wide, you'll only need to issue a single bus transaction to read/write one entry in the RAM. However, if you have a RAM that's ≥16 bits wide, you'll need to issue a bus transaction to update each 16-bit slice of it. For instance, updating a single entry in a 33-bit wide RAM would require sending 3 messages to the FPGA: one for bits 1-16, another for bits 17-32, and one for bit 33. If your application expects each RAM entry to update instantaneously, this could be problematic.
There's a few different ways to solve this - you could use an IO core to signal when a RAM's contents or valid - or you could ping-pong between two RAMs while one is being modified. The choice is yours, and Manta makes no attempt to prescribe any particular approach.
Lastly, the interface you use (and to a lesser extent, your operating system) will determine the space between bus transactions. For instance, 100Mbit Ethernet is a thousand times faster than 115200bps UART, so the time where the RAM is invalid is a thousand times smaller.
::: manta.MemoryCore

View File

@ -71,6 +71,7 @@ plugins:
handlers:
python:
options:
merge_init_into_class: true
heading_level: 3
show_bases: false
show_root_heading: true

View File

@ -9,9 +9,6 @@ class IOCore(MantaCore):
"""
A synthesizable module for setting and getting the values of registers of
arbitrary size.
Provides methods for generating synthesizable logic for the FPGA, as well
as methods for reading and writing the value of a register.
"""
def __init__(self, inputs=[], outputs=[]):
@ -24,12 +21,12 @@ class IOCore(MantaCore):
Args:
inputs (Optional[List[amaranth.Signal]]): A list of
Amaranth Signals to use as inputs. Defaults to an empty list.
This parameter is technically optional as an IO Core must have
This parameter is somewhat optional as an IO Core must have
at least one probe, but it need not be an input.
outputs (Optional[List[amaranth.Signal]]): A list of
Amaranth Signals to use as outputs. Defaults to an empty list.
This parameter is technically optional as an IO Core must have
This parameter is somewhat optional as an IO Core must have
at least one probe, but it need not be an output.
"""
@ -213,6 +210,8 @@ class IOCore(MantaCore):
"""
Set the value of an output probe on the FPGA.
This method is blocking.
Args:
probe (str | amaranth.Signal): The output probe to set the value
of. This may be either a string containing the name of the
@ -276,7 +275,8 @@ class IOCore(MantaCore):
If called on an output probe, this function will return the last value
written to the output probe. If no value has been written to the output
probe, then it will return the probe's initial value.
probe, then it will return the probe's initial value. This method is
blocking.
Args:
probe (str | amaranth.Signal): The probe to get the value of. This

View File

@ -8,17 +8,31 @@ from manta.utils import *
class MemoryCore(MantaCore):
"""
A module for generating a memory on the FPGA, with a port tied to Manta's
internal bus, and a port provided to user logic.
Provides methods for generating synthesizable logic for the FPGA, as well
as methods for reading and writing the value of a register.
More information available in the online documentation at:
https://fischermoseley.github.io/manta/memory_core/
A synthesizable module for accessing a memory. This is accomplished by
instantiating a dual-port memory with one end tied to Manta's internal bus,
and the other provided to user logic.
"""
def __init__(self, mode, width, depth):
"""
Create a Memory Core with the given width and depth.
This function is the main mechanism for configuring a Memory Core in an
Amaranth-native design.
Args:
mode (str): Must be one of `bidirectional`, `host_to_fpga`, or
'fpga_to_host'. Bidirectional memories can be both read or
written to by the host and FPGA, but they require the use
of a True Dual Port RAM, which is not available on all
platforms (most notably, the ice40). Host-to-fpga and
fpga-to-host RAMs only require a Simple Dual Port RAM, which
is available on nearly all platforms.
width (int): The width of the memory, in bits.
depth (int): The depth of the memory, in entries.
"""
self._mode = mode
self._width = width
self._depth = depth
@ -256,8 +270,24 @@ class MemoryCore(MantaCore):
def read(self, addrs):
"""
Read the memory stored at the provided address, as seen from the user
side.
Read the data stored in the Memory Core at one or many address.
This function can read from either one or multiple addresses at a time.
Due to the the IO latency in most OSes, a single multi-address read is
significantly faster than multiple single-address reads. Prefer their
usage where possible. This method is blocking.
Args:
addrs (int | List[int]): The memory address (or addresses) to read
from.
Returns:
datas (int | List[int]): The data stored at the address (or
addresses), represented as an unsigned integer.
Raises:
TypeError: addrs is not an integer or list of integers.
"""
# Handle a single integer address
@ -275,8 +305,27 @@ class MemoryCore(MantaCore):
def write(self, addrs, datas):
"""
Write to the memory stored at the provided address, as seen from the
user side.
Write data to the Memory core at one or many addresses.
This function can write to either one or multiple addresses at a time.
Due to the the IO latency in most OSes, a single multi-address write is
significantly faster than multiple single-address write. Prefer their
usage where possible. This method is blocking.
Args:
addrs (int | List[int]): The memory address (or addresses) to write
to.
datas (int | List[int]): The data to store at the address (or
addresses). This may be either positive or negative, but must
fit within the width of the memory.
Returns:
None
Raises:
TypeError: addrs or datas is not an integer or list of integers.
"""
# Handle a single integer address and data