This documentation file summarizes how to integrate a hardware peripheral in the x-heep platform.
This document features:
- Where to place the peripherals description files
- How to interface the peripheral in the x-heep platform
- How to implement the registers
- How to implement an RX window (for data stream)
- How to access the peripheral via software
- How to run a simulation
Note This document is a stub. You can contribute by detailing the implementation of other peripheral interfaces you encounter.
Peripherals RTL code are placed under :
hw/ip/<peripheral>
The module folder typically contains:
.
├── data
│ └── <peripheral>.hjson
├── rtl
│ ├── <peripheral>_reg_pkg.sv
│ ├── <peripheral>_reg_top.sv
│ ├── <peripheral>_reg_window.sv
│ ├── <peripheral>.sv
│ ├── <peripheral>_core.sv
│ └── ...
├── <peripheral>.core
├── <peripheral>.sh
└── <peripheral>.vlt
The <peripheral>.hjson
file is a manifest file that provides peripheral's metadata, bus interfaces, registers declaration and data windows declaration.
The <peripheral>_reg_pkg.sv
and <peripheral>_reg_top.sv
files are automatically generated at the peripheral build time.
The <peripheral>_reg_window.sv
file contains the hardware description of the data window. It it necessary only if the peripheral features such a window and can be merged into the peripheral hardware description. The naming is free.
The <peripheral>.sv
file is the top of the standalone peripheral, without the interface to the x-heep platform. The <peripheral>_core.sv
is a wrapper that interfaces the standalone peripheral with the signals coming from the auto-generated files. The naming is free and the files can be merged together but it is useful to keep them separated in order to separately simulate the standalone peripheral.
The <peripheral>.core
file is a manifest file that declares files and dependencies of the peripheral.
The <peripheral>.sh
file is a script file that is used to build the peripheral. It will generate the auto-generated files.
The <peripheral>.vlt
is a waiver file. By default, warnings make the simulation compilation fail. When a warning message is proven not to affect the robustness of the peripheral, a waiver can be added to this file in order to ignore the warning and resume the simulation compilation.
-
A
<peripheral>.hjson
file must be created (seehw/ip/pdm2pcm/data/pdm2pcm.hjson
for an example). See below for more details. -
A
<peripheral>.core
file must be created (seehw/ip/pdm2pcm/pdm2pcm.core
for an example). Dependencies must be declared underdepend:
and peripheral description files must be declared underfiles:
. -
A
<peripheral>.sh
file must be created. It can be adapted from the example given inOpenTitanIP.md#modified-generator
:
Warning When a file has a template (another file that has the same name but with a
.tpl
extension), this file is auto-generated. Therefore, do only edit the template file. Otherwise, the modifications will be overriden at the platform generation.
- In case of modification of the GPIOs usage, the
hw/fpga/xilinx_core_v_mini_mcu_wrapper.sv
must be adapted.
a. In the x_heep_system_i
instance, GPIOs can be replaced by the desired signals:
- .gpio_X_io(gpio_io[X]),
+ .your_signal_io(your_signal_io),
b. The module xilinx_core_v_mini_mcu_wrapper
should be modified as follows:
+ inout logic your_signal_io,
- inout logic [X:0] gpio_io,
+ inout logic [X-1:0] gpio_io,
c. The pads configuration (pad_cfg.json) must be adapted as well:
gpio: {
- num: <N>,
+ num: <N-D>,
num_offset: 0, #first gpio is gpio0
type: inout
},
+ pdm2pcm_pdm: {
+ num: 1,
+ type: inout
+ mux: {
+ <peripheral_io>: {
+ type: inout
+ },
+ gpio_K: {
+ type: inout
+ }
+ }
+ },
- The peripheral subsystem (
hw/core-v-mini-mcu/peripheral_subsystem.sv
) must also be adapted:
I. The I/O signals can be added in the peripheral_subsystem
module:
inout logic your_signal_io,
II. The module must be instantiated in the peripheral subsystem:
<peripheral> #(
.reg_req_t(reg_pkg::reg_req_t),
.reg_rsp_t(reg_pkg::reg_rsp_t)
) <peripheral>_i (
.clk_i,
.rst_ni,
<...>
);
- The core MCU I/O must be adapted as well (
hw/core-v-mini-mcu/core_v_mini_mcu.sv.tpl
). Add the I/Os to the peripherals subsystem instanciation:
peripheral_subsystem #(
<...>
) peripheral_subsystem_i (
<...>
+ .your_signal_io(your_signal_io),
<...>
);
- The peripheral package and the waiver files must be declared in the
core-v-mini-mcu.core
manifest:
depend:
<...>
- x-heep:ip:<peripheral>
files:
<...>
- hw/ip/<peripheral>/<peripheral>.vlt
- The MCU configuration (mcu_cfg.json) must be adapted:
peripherals: {
<...>
+ <peripheral>: {
+ offset: 0x00060000,
+ length: 0x00010000,
+ },
},
- Registers must be declared under the
registers:
list in the<peripheral>.hjson
file. To add a register, append the following:
{ name: "REGISTER_NAME"
desc: "Description"
swaccess: "rw"
hwaccess: "hro"
fields: [
{ bits: "15:0", name: "LSBITS", desc: "Those are the least significant bits." }
{ bits: "31:16", name: "MSBITS", desc: "Those are the most significant bits." }
]
}
- In order to access registers from hardware, the wrapper file (.sv) must be adapted:
a. The module interface must be as follows:
module <peripheral> #(
parameter type reg_req_t = logic,
parameter type reg_rsp_t = logic,
<...>
) (
input logic clk_i,
input logic rst_ni,
// Register interface
input reg_req_t reg_req_i,
output reg_rsp_t reg_rsp_o,
// I/Os
inout logic your_signal_io,
<...>
);
b. The corresponding package must be imported:
import <peripheral>_reg_pkg::*;
c. There are two objects used to interact with registers:
reg2hw
, that contains signals to read values from registers (hardware readable)hw2reg
, that contains signals to write values into the registers (hardware writable).
d. The signals used to interact with registers are the following:
q
contains the current value of a register to be read by hardwared
is written to assign a value to the registerde
is an enable signal to get the value written by the hardware from software. It must be asserted to1
to be able to read it from software.
e. A register with only one filed is accessed by its name and a register with several fields is accessed by its fields.
f. Some examples:
hw2reg.register.field.de // Data enable signal of a hardware-written field
hw2reg.register.d // Data to be written on a hardware-written register
reg2hw.register.q // Data to be read from a register
- Windows must be declared under the
registers:
list in the<peripheral>.hjson
file. To add a window, append the following:
{ window: {
name: "RX_WINDOW_NAME",
items: "1",
validbits: "32",
desc: '''Window purpose description'''
swaccess: "ro"
}
- An RX window typically looks as follows:
module <peripheral>_window #(
parameter type reg_req_t = logic,
parameter type reg_rsp_t = logic
) (
input reg_req_t rx_win_i,
output reg_rsp_t rx_win_o,
input [31:0] rx_data_i,
output logic rx_ready_o
);
import <peripheral>_reg_pkg::*;
logic [BlockAw-1:0] rx_addr;
logic rx_win_error;
assign rx_win_error = (rx_win_i.write == 1'b1) && (rx_addr != <peripheral>_reg_pkg::<peripheral>_RXDATA_OFFSET);
assign rx_ready_o = rx_win_i.valid & ~rx_win_i.write;
assign rx_win_o.rdata = rx_data_i;
assign rx_win_o.error = rx_win_error;
assign rx_win_o.ready = 1'b1;
assign rx_addr = rx_win_i.addr;
endmodule : <peripheral>_window
- Data is presented on the
rx_data_i
signal andrx_ready_o
is asserted.
- Include the following headers to get the peripherals addresses macros and the I/O manipulation functions:
#include "core_v_mini_mcu.h"
#include "<peripheral>_regs.h"
#include "mmio.h"
- The function used to get the base address of the peripheral is the following (the base address is defined in
sw/device/lib/runtime/core_v_mini_mcu.h.tpl
):
mmio_region_t <peripheral>_base_addr = mmio_region_from_addr((uintptr_t)<peripheral>_START_ADDRESS);
- The read and write functions are the following:
mmio_region_write32(pdm2pcm_base_addr, <peripheral>_REGISTERNAME_REG_OFFSET ,<value_to_write>);
uint32_t response = mmio_region_read32(<peripheral>_base_addr, <peripheral>_REGISTERNAME_REG_OFFSET);
Use the hello_world
(sw/applications/hello_world
) program to quickly test a design.
The hardware platform is contained in the testharness.sv
(tb/testharness.sv) test bench.
If the GPIOs usage has changed, the testbench must be adapted as follows:
- .gpio_X_io(gpio[X]),
+ .your_signal_io(gpio[X]),
You must register the interrupt in the MCU configuration mcu_cfg.json
.
interrupts: {
number: 64, // Do not change this number!
list: {
...
+ <interrupt identifier>: <interrupt num>
}
}
and then connect your signal in peripheral_subsystem.sv
to the plic
+ assign intr_vector[${interrupts["<interrupt identifier>"]}] = <your signal>;
In software the interrupt gets trigger as "external" interrupt see rv_plic
documentation, your interrupt number to be used in c is automatically added to the core_v_mini_mcu.h
under the preprosessor define <YOUR INTERRUPT>
.