ChAI is a reference implementation of an embedded system, build around the Kelvin CPU. It is intended as a starting point for either building out a more complete system, or integrating into an existing system
The SystemVerilog for ChAI can be built with the following command:
bazel build //hdl/chisel/src/chai:chai_cc_library_emit_verilog
The generated file will be in bazel-bin/hdl/chisel/src/chai/ChAI.sv
You can run program on the ChAI verilator sim with:
bazel run //tests/verilator_sim:chai_sim -- \ --cycles 2000 \ /path/to/program.bin
In ChAI.scala, add a new entry to the memoryRegions table for your peripheral:
val memoryRegions = Seq( new kelvin.MemoryRegion(0, 4 * 1024 * 1024, true, 256), // SRAM new kelvin.MemoryRegion(4 * 1024 * 1024, 4 * 1024 * 1024, false, 256) // UART new kelvin.MemoryRegion(8 * 1024 * 1024, 1 * 1024 * 1024, false, 256) // My New Peripheral )
A MemoryRegion consists of a starting address (in bytes), a size (in bytes), whether or not the region is cacheable (likely only to be true for a memory peripheral), and an access size (ignored today, all accesses are 256-bit)
In ChAI.scala, connect your device's TileLink I/O to the crossbar:
val crossbar = Module(new kelvin.TileLinkUL(tlul_p, kelvin_p.m, /* hosts= */ 1)) crossbar.io.hosts_a(0) <> kelvin_to_tlul.io.tl_o crossbar.io.hosts_d(0) <> kelvin_to_tlul.io.tl_i crossbar.io.devices_a(0) <> tlul_adapter_sram.io.tl_i crossbar.io.devices_d(0) <> tlul_adapter_sram.io.tl_o crossbar.io.devices_a(1) <> uart.io.tl_i crossbar.io.devices_d(1) <> uart.io.tl_o crossbar.io.devices_a(2) <> my_new_peripheral.io.tl_i crossbar.io.devices_d(2) <> my_new_peripheral.io.tl_o
Here's a skeleton of a peripheral that has a single register that can be read or written.
package chai import chisel3._ import chisel3.util._ object DummyPeripheral { def apply(tlul_p: kelvin.TLULParameters): DummyPeripheral = { return Module(new DummyPeripheral(tlul_p)) } } class DummyPeripheral(tlul_p: kelvin.TLULParameters) extends Module { val io = IO(new Bundle { val tl_i = Input(new kelvin.TileLinkULIO_H2D(tlul_p)) val tl_o = Output(new kelvin.TileLinkULIO_D2H(tlul_p)) }) io.tl_o := 0.U.asTypeOf(new kelvin.TileLinkULIO_D2H(tlul_p)) val saved_value = RegInit(0.U(32.W)) val tl_i_reg = Reg(new kelvin.TileLinkULIO_H2D(tlul_p)) tl_i_reg := io.tl_i io.tl_o.a_ready := true.B when (tl_i_reg.a_valid) { io.tl_o.d_valid := true.B switch (tl_i_reg.a_opcode) { is (4.U /* kelvin.TLULOpcodesA.Get.asUInt */) { io.tl_o.d_opcode := kelvin.TLULOpcodesD.AccessAckData.asUInt io.tl_o.d_param := 0.U io.tl_o.d_size := tl_i_reg.a_size io.tl_o.d_source := tl_i_reg.a_source io.tl_o.d_sink := 1.U io.tl_o.d_data := saved_value io.tl_o.d_error := false.B } is (0.U /* PutFullData */) { saved_value := tl_i_reg.a_data io.tl_o.d_opcode := kelvin.TLULOpcodesD.AccessAck.asUInt io.tl_o.d_param := 0.U io.tl_o.d_size := tl_i_reg.a_size io.tl_o.d_source := tl_i_reg.a_source io.tl_o.d_sink := 1.U io.tl_o.d_data := 0.U io.tl_o.d_error := false.B } is (1.U /* PutPartialData */) { /* no-op */ } } } }