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:

  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 */
            }
        }
    }
}