Platform Devices

CLINT, PLIC, HTIF, and FDT generation on RISC-V

Data Flow

graph TD A[Timer IRQ] --> B[CLINT] B --> C[set_irq -> mip.MTIP] D[Device IRQ] --> E[PLIC] E --> F[plic_set_irq -> mip.SEIP/MEIP] G[HTIF Console] --> H[htif_read / htif_write] H --> I[Host console / poweroff] J[Machine Init] --> K[riscv_build_fdt] K --> L[Device Tree Blob in RAM]

CLINT — Core Local Interruptor

The CLINT provides timer interrupts and software interrupts. It is mapped at CLINT_BASE_ADDR. Reading mtimecmp and writing mtime allows the guest to schedule timer interrupts. When the host real-time clock exceeds mtimecmp, the CLINT raises MTIP in mip.

static uint32_t clint_read(void *opaque, uint32_t offset, int size_log2)
{
    RISCVMachine *s = opaque;
    switch(offset) {
    case 0x4000 ... 0x4007:
        /* mtimecmp low/high */
        return ...;
    case 0xbff8 ... 0xbfff:
        /* mtime low/high */
        return rtc_get_time(s);
    }
}

static void clint_write(void *opaque, uint32_t offset,
                        uint32_t val, int size_log2)
{
    RISCVMachine *s = opaque;
    switch(offset) {
    case 0x4000 ... 0x4007:
        /* set mtimecmp */
        s->timecmp = ...;
        break;
    }
}

PLIC — Platform Level Interrupt Controller

The PLIC aggregates up to 32 external interrupt sources. Each source has a priority, a pending bit, and an enable bit per context. When a device raises an IRQ, plic_set_irq updates the pending register and asserts the CPU interrupt line.

static uint32_t plic_read(void *opaque, uint32_t offset, int size_log2)
{
    RISCVMachine *s = opaque;
    if (offset >= PLIC_HART_BASE) {
        /* context threshold / claim/complete */
    } else {
        /* priority / pending / enable registers */
    }
}

static void plic_write(void *opaque, uint32_t offset,
                       uint32_t val, int size_log2)
{
    /* threshold, claim/complete, enable */
}

static void plic_set_irq(void *opaque, int irq_num, int level)
{
    RISCVMachine *s = opaque;
    if (level) {
        s->plic_pending_irq |= (1 << irq_num);
    } else {
        s->plic_pending_irq &= ~(1 << irq_num);
    }
    plic_update_mip(s);
}

HTIF — Host Target Interface

HTIF is a legacy mechanism for console output and system poweroff. TinyEMU uses fixed addresses (0x40008000) instead of ELF-symbol-based addresses. The bootloader was patched to support this.

static uint32_t htif_read(void *opaque, uint32_t offset, int size_log2)
{
    RISCVMachine *s = opaque;
    if (offset == 0) return s->htif_tohost;
    if (offset == 8) return s->htif_fromhost;
    return 0;
}

static void htif_write(void *opaque, uint32_t offset,
                       uint32_t val, int size_log2)
{
    RISCVMachine *s = opaque;
    if (offset == 0) {
        s->htif_tohost = val;
        htif_handle_cmd(s);
    }
}

static void htif_handle_cmd(RISCVMachine *s)
{
    uint64_t cmd = s->htif_tohost;
    int device = (cmd >> 56) & 0xff;
    int cmd_code = (cmd >> 48) & 0xff;
    if (device == 1 && cmd_code == 1) {
        /* console output */
        console_write(s->common.console, (char)(cmd & 0xff));
    } else if (device == 0 && cmd_code == 0) {
        /* poweroff */
        exit(0);
    }
}

FDT — Flattened Device Tree

Before boot, riscv_build_fdt() constructs a device tree blob in memory describing CPUs, memory, CLINT, PLIC, VirtIO devices, and the framebuffer. The BIOS reads this FDT to discover hardware.

int riscv_build_fdt(RISCVMachine *s, uint8_t *dst,
                    uint64_t kernel_start, uint64_t kernel_size,
                    uint64_t initrd_start, uint64_t initrd_size,
                    const char *cmdline)
{
    FDTState *s = fdt_init();
    fdt_begin_node(s, "");
    fdt_prop_u32(s, "#address-cells", 2);
    fdt_prop_u32(s, "#size-cells", 2);
    fdt_prop_str(s, "compatible", "riscv-virtio");
    /* cpus, memory, soc, virtio, chosen ... */
    fdt_end_node(s);
    return fdt_output(s, dst);
}