Integrate a Peripheral IP
This guide walks through the end-to-end flow to add a new memory-mapped peripheral to X-HEEP. We use the existing dlc IP as the blueprint because it exercises the full hardware, software, and tooling stack.
Set up the X-HEEP environment by following the Getting Started guide.
1. Create the IP Structure
This is how the folder should look like:
└── hw/ip/<peripheral>
├── data/
│ └──<peripheral>.hjson # register description (for regtool generation)
├── rtl/
│ ├──<peripheral>.sv # RTL of the peripheral
│ ├── <peripheral>_reg_top.sv # generated by regtool
│ └── <peripheral>_reg_pkg.sv # generated by regtool
├── <peripheral>.core # FuseSoC description
├── <peripheral>.vlt # Verilator waiver
└── <peripheral>_gen.sh # helper script running regtool
X-HEEP-compatible module ports
All X-HEEP peripheral modules require specific ports that allows it to communicate with the rest of the system.
The following is the absolute minimum starting point to develop your peripheral module ports:
module peripheral (
input logic clk_i,
input logic rst_ni,
// Register interface
input reg_pkg::reg_req_t reg_req_i,
output reg_pkg::reg_rsp_t reg_rsp_o
);
The CPU can access the configuration registers of the peripheral via the register interface:
reg_req_iis the input request, which can either request to write some value into a register or read values from a registerreg_rsp_ois the output response, which may contains register data if the request was a read operation
In any case, the implementation of this protocol is transparent to the RTL designer, as its included in the top register module generated via regtool depending on the data/<peripheral>.hjson information.
Here is an extended example of a module port structure which includes additional features:
module peripheral (
input logic clk_i,
input logic rst_ni,
// Register interface
input reg_pkg::reg_req_t reg_req_i,
output reg_pkg::reg_rsp_t reg_rsp_o,
// Done signal
output logic peripheral_done_o,
// Interrupt signal
output logic peripheral_interrupt_o,
// Master ports on the system bus
output obi_req_t peripheral_master_bus_req_o,
input obi_resp_t peripheral_master_bus_resp_i
);
About the additional features:
peripheral_done_o: this done signal could be useful if you want X-HEEP to expose it, so that external units can be aware of it. For example, each X-HEEP’s DMA channel exposes adma_done_osignal. This could be used for example by accelerators that exploit the DMA functionalities to move data in and out of their local memoryperipheral_interrupt_o: this signal can be used to set up a dedicated interrupt line reserved for your peripheral. This would allow the execution of custom interrupt handlers in SW, a common and useful feature.peripheral_master_bus_req_o / peripheral_master_bus_resp_i: these are OBI request and response signals, necessary if you want your peripheral to be a master on the system bus. In other words, this would allow your peripheral to read and write data in every memory mapped space of X-HEEP. For example, this could allow your peripheral to read from RAM, or it could allow it to configure other peripherals.
2. Describe and Generate Registers
In X-HEEP, registers are automatically generated using RegTool by OpenTitan. Please refer to the official documentation for additional information: https://opentitan.org/book/util/reggen/index.html. In anycase, this tool will generate both RTL and SW components starting from an HJSON description. These are the steps needed to set everything up:
Populate
data/<peripheral>.hjsonwith registers, fields, reset values, and optional interrupts. DLC’sdlc.hjsonis a good example, but you could take inspiration from any X-HEEP.hjson.Generate RTL wrappers and the C header files by running the
<peripheral>_gen.shscript.
3. Expose the IP to FuseSoC
This is a crucial step in integrating the new peripheral in X-HEEP’s flow:
Adapt
hw/ip/<peripheral>/<peripheral>.corefromdlc.core. List all RTL files underfiles:and declare dependencies (for examplepulp-platform.org::register_interface).Add the peripheral as a dependency of the SoC in
core-v-mini-mcu.core:filesets: files_rtl_generic: depend: - example:ip:<peripheral> # use the namespace chosen in your .core file
If the IP has lint waivers for Verilator, reference the waiver in the
.corefile:filesets: depend: - openhwgroup.org:ip:verilator_waiver files: - hw/ip/<peripheral>/<peripheral>.vlt # use the namespace chosen in your .core file
4. Reserve Address Space in the Configuration
This is another crucial step in the integration of the peripheral. Please be aware that the address map of X-HEEP might change in the future, so consider this for the addresses of the following examples.
As said at the beginning of this guide, these steps are the same for any domain you want to integrate the peripheral in, both user peripherals and base peripherals.
Extend the configuration you built (HJSON or Python) with a new entry in the preferred domain. Example (
configs/general.hjson):peripherals: { address: 0x30000000 length: 0x00100000 // existing peripherals… <peripheral>: { offset: 0x00080000 length: 0x00010000 is_included: "yes" } // existing peripherals… }
For Python configs, import a new class in
util/xheep_gen/peripherals/user_peripherals.py(orbase_peripherals.pyif mandatory):class MyPeripheral(UserPeripheral): _name = "my_peripheral"
Instantiate the class in your config script and add it to the user domain before calling
build().If your IP generates an interrupt signal, extend the interrupt list with it.
Warning
This step is necessary only for peripheral domain IPs.
interrupts: {
number: 64 // Do not change this number!
list: {
// First one is always zero
null_intr: 0
uart_intr_tx_watermark: 1
// Other interrupt signals...
i2s_intr_event: 50
// Your own peripheral:
<peripheral>_intr_event: 51
}
}
5. Integrate the IP in the Peripheral domain
Now, let’s instantiate the peripheral in the peripheral_subsystem.sv.tpl, as the .sv will be regenerated every make mcu-gen ran.
As a good reference, please look at the i2s peripheral. There are some important considerations to make, so this is an example of a peripheral instantiation that mirrors the previous examples:
peripheral_ip #(
.reg_req_t(reg_pkg::reg_req_t),
.reg_rsp_t(reg_pkg::reg_rsp_t)
) peripheral_ip_i (
.clk_i(clk_cg),
.rst_ni,
// Register interface
.reg_req_i(peripheral_slv_req[core_v_mini_mcu_pkg::PERIPHERL_IDX]),
.reg_rsp_o(peripheral_slv_rsp[core_v_mini_mcu_pkg::PERIPHERL_IDX]),
// Done signal
.peripheral_done_o,
// Interrupt signal
peripheral_interrupt_o(peripheral_intr_event),
// Master ports on the system bus
.peripheral_master_bus_req_o,
.peripheral_master_bus_resp_i
);
The ports of the peripheral subsystem itself have to be changed too if you want to add a master port on the bus or if you want to expose a signal:
module peripheral_subsystem
import obi_pkg::*;
import reg_pkg::*;
#(
//do not touch these parameters
parameter NEXT_INT_RND = core_v_mini_mcu_pkg::NEXT_INT == 0 ? 1 : core_v_mini_mcu_pkg::NEXT_INT
) (
input logic clk_i,
input logic rst_ni,
// Other signals...
// PDM2PCM Interface
output logic pdm2pcm_clk_o,
output logic pdm2pcm_clk_en_o,
input logic pdm2pcm_pdm_i,
// Your peripheral signals
output logic peripheral_done_o,
output obi_req_t peripheral_master_bus_req_o,
input obi_rsp_t peripheral_master_bus_rsp_i
);
Some notes:
clk_iis connected toclk_cg, as the peripheral subsystem can be clock gated. If you want to clock gate individually your peripheral, implement that feature inside your IP.reg_req_i / reg_rsp_oare connected to a register-interface demultiplexer which routes all requests from the bus to the correct peripheral based on their address map. That’s why it’s very important that you select the correctperipheral_slv_reqandperipheral_slv_rspsignal with the ID associated with your peripheral.peripheral_done_o,peripheral_master_bus_req_oandperipheral_master_bus_rsp_iconnects the module’s ports to the peripheral’s ports.
If your peripheral need a new system bus master port, like in our example, you need to modify other files too. In order:
core_v_mini_pkg.sv.tpl: this file generates parameter for many X-HEEP modules, included the system bus. In particular, we are interested in increasing theSYSTEM_XBAR_NMASTERparameter by the number of master ports we need. In our example, one is sufficient.system_bus.sv.tpl: this is the template for the system bus. Here we need to add a new port of the module and connectint_master_reqandint_master_rspsignals to the new port:
assign int_master_req[9] = peripheral_master_bus_req_i;
// Other code...
assign peripheral_master_bus_req_o = int_master_resp[9];
core_v_mini_mcu.sv.tpl: finally, we need to update thesystem_businstantiation, and connect the peripheral master bus signals to the system bus ones.
As a good reference for this entire process, consider once again the DMA subsystem.
Regardless of system bus master ports, we need to update the peripheral_susystem or ao_peripheral_subsystem instantiation in core_v_mini_mcu.sv.tpl to match out modifications.
Furthermore, if your peripheral needs to expose a signal, like peripheral_done_o in our case, we need to perform some additional modifications. In order:
Add to the ports of the
core_v_mini_mcumodule the signal itselfUpdate
x_heep_system.sv.tplto match the newcore_v_mini_mcuportsUpdate
testharness.svto match the newx_heep_systemports
Finally, if you want, in testharness.sv you can instantiate an accelerator that can exploit the peripheral_done_o signal.
Warning
After updating all these templates, run make mcu-gen to regenerate them!
6. Hook Up FPGA Wrappers
Edit the board wrapper templates (e.g.
hw/fpga/xilinx_core_v_mini_mcu_wrapper.sv.tpl) to expose the new signals and regenerate the concrete.svfile.Update board constraint files or scripts (such as
hw/fpga/scripts/generate_sram.tcl.tpl) when the IP drives physical I/O.Re-run
make mcu-gento propagate template changes before rebuilding bitstreams.
7. Provide Software Accessors
In order to simplify the user experience, create a driver in sw/device/lib/drivers/<peripheral>/. We suggest taking as a reference the structure of sw/device/lib/drivers/dlc, but feel free to be inspired by any X-HEEP driver module. Typical contents:
<peripheral>.c/<peripheral>.h– MMIO helper functions.<peripheral>.h(generated earlier viaregtool).<peripheral>_structs.h– emitted byutil/structs_periph_gen.pyif needed.
8. Add Firmware Examples and Tests
This is strongly suggested, as it can serve both as a guide for future developers and a debugging tool.
To do so, write an example application under sw/applications/example_<name>/ that exercises the new driver.
9. (For pull requests) Update Documentation
This is mandatory for any new IP that you would like to propose to be included in the official X-HEEP repository.
Students, companies and passionate individuals can all contribute to X-HEEP, and we encourage you to do so.
However, we need to keep this project maintainable. For this reason, please add a dedicated page in the Peripherals folder of the documentation.
For reference, please check out our guide on how to update the documentation here.
Checklist
[ ] IP directory added under
hw/ip/<name>(orhw/ip_examples/<name>) with.core, RTL, and HJSON files.[ ]
regtoolexecuted; generated RTL and headers checked in.[ ] Configuration (HJSON or Python) updated with offset, length, and optional interrupt entries.
[ ] Templates updated and
make mcu-genrun; regenerated outputs reviewed.[ ]
peripheral_subsystem(and other templates) instantiate and connect the IP.[ ] Testbench and FPGA wrappers handle the new signals.
[ ] Driver (
sw/device/lib/drivers/<name>) implemented; structs regenerated if needed.[ ] Example firmware compiled and simulations pass.
With these steps complete, your peripheral is fully integrated into the X-HEEP hardware, verification, and software flow.