| # What is ChAI? |
| |
| 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 |
| |
| ## Building ChAI RTL |
| |
| 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` |
| |
| ## Running a simulating ChAI |
| |
| You can run program on the ChAI verilator sim with: |
| |
| ``` |
| bazel run //tests/verilator_sim:chai_sim -- \ |
| --cycles 2000 \ |
| /path/to/program.bin |
| ``` |
| |
| ## Components |
| - Kelvin CPU |
| - TileLink-UL crossbar |
| - UART (from lowRISC) |
| - SRAM (from Chisel, and using an adapter from lowRISC) |
| |
| ## Limitations |
| - Can only address memory at 32B / 256b offsets (existing peripherals may require modifications due to this) |
| - No interrupt support |
| |
| ## Adding a new peripheral |
| In ChAI.scala, add a new entry to the memoryRegions table for your peripheral: |
| ```scala |
| val memoryRegions = Seq( |
| new kelvin.MemoryRegion(0, 4 * 1024 * 1024, MemoryType.DMEM), // SRAM |
| new kelvin.MemoryRegion(4 * 1024 * 1024, 4 * 1024 * 1024, MemoryType.Peripheral) // UART |
| new kelvin.MemoryRegion(8 * 1024 * 1024, 1 * 1024 * 1024, MemoryType.Peripheral) // My New Peripheral |
| ) |
| ``` |
| A MemoryRegion consists of a starting address (in bytes), a size (in bytes), and the region type (IMEM, DMEM, Peripheral). |
| |
| In ChAI.scala, connect your device's TileLink I/O to the crossbar: |
| ```scala |
| 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. |
| ```scala |
| 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 */ |
| } |
| } |
| } |
| } |
| ``` |