diff --git a/hw/intc/Kconfig b/hw/intc/Kconfig
index dd405bdb5d..1cd4c2f58c 100644
--- a/hw/intc/Kconfig
+++ b/hw/intc/Kconfig
@@ -81,6 +81,9 @@ config SIFIVE_PLIC
     bool
     select MSI_NONBROKEN

+config RISCV_CLIC
+    bool
+
 config GOLDFISH_PIC
     bool

diff --git a/hw/intc/meson.build b/hw/intc/meson.build
index f4d81eb8e4..a9207dfb9e 100644
--- a/hw/intc/meson.build
+++ b/hw/intc/meson.build
@@ -59,9 +59,10 @@ specific_ss.add(when: 'CONFIG_S390_FLIC_KVM', if_true: files('s390_flic_kvm.c'))
 specific_ss.add(when: 'CONFIG_SH_INTC', if_true: files('sh_intc.c'))
 specific_ss.add(when: 'CONFIG_RISCV_ACLINT', if_true: files('riscv_aclint.c'))
 specific_ss.add(when: 'CONFIG_RISCV_APLIC', if_true: files('riscv_aplic.c'))
+specific_ss.add(when: 'CONFIG_RISCV_CLIC', if_true: files('riscv_clic.c'))
 specific_ss.add(when: 'CONFIG_RISCV_IMSIC', if_true: files('riscv_imsic.c'))
 specific_ss.add(when: 'CONFIG_SIFIVE_PLIC', if_true: files('sifive_plic.c'))
-specific_ss.add(when: 'CONFIG_XICS', if_true: files('xics.c', 'xive2.c'))
+specific_ss.add(when: 'CONFIG_XICS', if_true: files('xics.c'))
 specific_ss.add(when: ['CONFIG_KVM', 'CONFIG_XICS'],
                if_true: files('xics_kvm.c'))
 specific_ss.add(when: 'CONFIG_PSERIES', if_true: files('xics_spapr.c', 'spapr_xive.c'))
diff --git a/hw/intc/riscv_clic.c b/hw/intc/riscv_clic.c
new file mode 100644
index 0000000000..1800e84dfd
--- /dev/null
+++ b/hw/intc/riscv_clic.c
@@ -0,0 +1,1037 @@
+/*
+ * RISC-V CLIC(Core Local Interrupt Controller) for QEMU.
+ *
+ * Copyright (c) 2016-2017 Sagar Karandikar, sagark@eecs.berkeley.edu
+ * Copyright (c) 2017-2018 SiFive, Inc.
+ * Copyright (c) 2021 T-Head Semiconductor Co., Ltd. All rights reserved.
+ * Copyright (c) 2024 Cirrus Logic, Inc and
+ *      Cirrus Logic International Semiconductor Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2 or later, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * This implementation follows the CLIC 0.9-stable draft at 14 March 2024,
+ * with the following exceptions and implementation details:
+ *  - the CLIC control registers are memory-mapped as per earlier drafts (in
+ *    particular version 0.9-draft, 20 June 2023)
+ *  - the indirect CSR control in 0.9-stable is not implemented
+ *  - the vector table can be either handler addresses (as per the spec)
+      or a jump table where each entry is processed as an instruction,
+      selectable with version number v0.9-jmp
+ *  - each hart is assigned its own CLIC block
+ *  - support for PRV_S and PRV_M is selectable at CLIC instantiation by
+ *    passing in a base address for the given modes; a base address of 0 is
+ *    treated as not supported
+ *  - PRV_S and PRV_U registers are mapped  onto the PRV_M controls with
+ *    appropriate filtering for the access mode
+ *
+ * The implementation has a RISCVCLICState per hart, with a RISCVCLICView
+ * for each mode subsidiary to that. Each view knows its access mode and base
+ * address, as well as the RISCVCLICState with which it is associated.
+ *
+ * MMIO accesses go through the view, allowing the appropriate permissions to
+ * be enforced when accessing the parent RISCVCLICState for the settings.
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qemu/log.h"
+#include "qemu/main-loop.h"
+#include "hw/sysbus.h"
+#include "sysemu/qtest.h"
+#include "target/riscv/cpu.h"
+#include "hw/qdev-properties.h"
+#include "hw/intc/riscv_clic.h"
+
+static const char *modeview_name[] = {
+    TYPE_RISCV_CLIC "_prv_u",       /* PRV_U */
+    TYPE_RISCV_CLIC "_prv_s",       /* PRV_S */
+    NULL,                           /* reserved */
+    TYPE_RISCV_CLIC "_prv_m",       /* PRV_M */
+};
+
+/*
+ * The 2-bit trig WARL field specifies the trigger type and polarity for each
+ * interrupt input. Bit 1, trig[0], is defined as "edge-triggered"
+ * (0: level-triggered, 1: edge-triggered); while bit 2, trig[1], is defined as
+ * "negative-edge" (0: positive-edge, 1: negative-edge). (Section 3.6)
+ */
+
+static inline TRIG_TYPE
+riscv_clic_get_trigger_type(RISCVCLICState *clic, size_t irq)
+{
+    return get_field(clic->clicintattr[irq], CLIC_INTATTR_TRIG);
+}
+
+static inline bool
+riscv_clic_is_edge_triggered(RISCVCLICState *clic, size_t irq)
+{
+    TRIG_TYPE trig_type = riscv_clic_get_trigger_type(clic, irq);
+    return trig_type & CLIC_INTATTR_TRIG_EDGE;
+}
+
+static inline bool
+riscv_clic_is_shv_interrupt(RISCVCLICState *clic, size_t irq)
+{
+    uint32_t shv = get_field(clic->clicintattr[irq], CLIC_INTATTR_SHV);
+    return shv && clic->shv_enabled;
+}
+
+static uint8_t
+riscv_clic_get_interrupt_level(RISCVCLICState *clic, uint8_t intctl)
+{
+    int nlbits = min(clic->mnlbits, clic->clicintctlbits);
+
+    uint8_t mask_il = ((1 << nlbits) - 1) << (8 - nlbits);
+    uint8_t mask_padding = (1 << (8 - nlbits)) - 1;
+    /* unused level bits are set to 1 */
+    return (intctl & mask_il) | mask_padding;
+}
+
+static uint8_t
+riscv_clic_get_interrupt_priority(RISCVCLICState *clic, uint8_t intctl)
+{
+    int npbits = clic->clicintctlbits - clic->mnlbits;
+    uint8_t mask_priority = ((1 << npbits) - 1) << (8 - npbits);
+    uint8_t mask_padding = (1 << (8 - npbits)) - 1;
+
+    if (npbits < 0) {
+        return UINT8_MAX;
+    }
+    /* unused priority bits are set to 1 */
+    return (intctl & mask_priority) | mask_padding;
+}
+
+static void
+riscv_clic_intcfg_decode(RISCVCLICState *clic, uint16_t intcfg,
+                         uint8_t *mode, uint8_t *level,
+                         uint8_t *priority)
+{
+    *mode = intcfg >> 8;
+    *level = riscv_clic_get_interrupt_level(clic, intcfg & 0xff);
+    *priority = riscv_clic_get_interrupt_priority(clic, intcfg & 0xff);
+}
+
+static void riscv_clic_next_interrupt(void *opaque)
+{
+    /*
+     * Scan active list for highest priority pending interrupts
+     * comparing against this harts mintstatus register and interrupt
+     * the core if we have a higher priority interrupt to deliver
+     */
+    RISCVCLICState *clic = (RISCVCLICState *)opaque;
+    CPUState *cpu = cpu_by_arch_id(clic->hartid);
+    CPURISCVState *env = cpu ? cpu_env(cpu) : NULL;
+
+    if (!env) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "aclint-swi: invalid hartid: %u", clic->hartid);
+        return;
+    }
+
+    BQL_LOCK_GUARD();
+
+    int il[4] = {
+        MAX(get_field(env->mintstatus, MINTSTATUS_UIL),
+            clic->uintthresh & 0xff), /* PRV_U */
+        MAX(get_field(env->mintstatus, MINTSTATUS_SIL),
+            clic->sintthresh & 0xff), /* PRV_S */
+        0,                     /* reserved */
+        MAX(get_field(env->mintstatus, MINTSTATUS_MIL),
+            clic->mintthresh & 0xff)  /* PRV_M */
+    };
+
+    /* Get sorted list of enabled interrupts for this hart */
+    CLICActiveInterrupt *active = clic->active_list;
+    size_t active_count = clic->active_count;
+    uint8_t mode, level, priority;
+
+    /* Loop through the enabled interrupts sorted by mode+priority+level */
+    while (active_count) {
+        riscv_clic_intcfg_decode(clic, active->intcfg, &mode, &level,
+                                 &priority);
+        if (mode < env->priv || (mode == env->priv && level < il[mode])) {
+            /*
+             * No pending interrupts with high enough mode+priority+level
+             * break and clear pending interrupt for this hart
+             */
+            break;
+        }
+        /* Check pending interrupt with high enough mode+priority+level */
+        if (clic->clicintip[active->irq]) {
+            /* Clean vector edge-triggered pending */
+            if (riscv_clic_is_edge_triggered(clic, active->irq) &&
+                riscv_clic_is_shv_interrupt(clic, active->irq)) {
+                clic->clicintip[active->irq] = 0;
+            }
+            /* Post pending interrupt for this hart */
+            clic->exccode = active->irq |
+                            mode << RISCV_EXCP_CLIC_MODE_SHIFT |
+                            level << RISCV_EXCP_CLIC_LEVEL_SHIFT;
+            qemu_set_irq(clic->cpu_irq, 1);
+            return;
+        }
+        /* Check next enabled interrupt */
+        active_count--;
+        active++;
+    }
+}
+
+/*
+ * Any interrupt i that is not accessible to S-mode or U-Mode
+ * appears as hard-wired zeros in clicintip[i], clicintie[i],
+ * clicintattr[i], and clicintctl[i].(Section 3.9)(Section 3.10)
+ */
+static bool
+riscv_clic_check_visible(RISCVCLICState *clic, int mode, int irq)
+{
+    uint8_t intattr_mode = get_field(clic->clicintattr[irq],
+                                     CLIC_INTATTR_MODE);
+
+    if (!clic->prv_s && !clic->prv_u) { /* M */
+        return mode == PRV_M;
+    } else if (clic->prv_s && clic->prv_u) { /* M/S/U */
+        switch (clic->nmbits) {
+        case 0:
+            return mode == PRV_M;
+        case 1:
+            return (mode == PRV_M) || (intattr_mode <= PRV_S);
+        case 2:
+            return mode >= intattr_mode;
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR,
+                "clic: nmbits can only be 0 or 1 or 2 for M/S/U hart");
+            exit(1);
+        }
+    } else { /* M/S or M/U */
+        switch (clic->nmbits) {
+        case 0:
+            return mode == PRV_M;
+        case 1:
+            return (mode == PRV_M) || (intattr_mode <= PRV_S);
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR,
+                "clic: nmbits can only be 0 or 1 for M/S or M/U hart");
+            exit(1);
+        }
+    }
+    return false;
+}
+
+/*
+ * For level-triggered interrupts, software writes to pending bits are
+ * ignored completely. (Section 3.4)
+ */
+static bool
+riscv_clic_validate_intip(RISCVCLICState *clic, int irq)
+{
+    return riscv_clic_is_edge_triggered(clic, irq);
+}
+
+static void
+riscv_clic_update_intip(RISCVCLICState *clic, int irq, uint64_t value)
+{
+    clic->clicintip[irq] = !!value;
+    riscv_clic_next_interrupt(clic);
+}
+
+/*
+ * For security purpose, the field can only be set to a privilege
+ * level that is equal mode to or lower than the currently running
+ * privilege level.(Section 3.6)
+ */
+static bool riscv_clic_validate_intattr(RISCVCLICState *clic, uint8_t value)
+{
+    int mode = extract64(value, CLIC_INTATTR_MODE_SHIFT,
+                         CLIC_INTATTR_MODE_WIDTH);
+
+    if (!qtest_enabled()) {
+        CPURISCVState *env = cpu_env(current_cpu);
+        if (env->priv < mode) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/*
+ * Work out the effective requested mode based on the number of nmbits.
+ *
+ * priv-modes nmbits mode Interpretation
+ * M            0     xx  M-mode interrupt
+ *
+ * M/U          0     xx  M-mode interrupt
+ * M/U          1     0x  U-mode interrupt
+ * M/U          1     1x  M-mode interrupt
+ *
+ * M/S          0     xx  M-mode interrupt
+ * M/S          1     0x  S-mode interrupt
+ * M/S          1     1x  M-mode interrupt
+ *
+ * M/S/U        0     xx  M-mode interrupt
+ * M/S/U        1     0x  S-mode interrupt
+ * M/S/U        1     1x  M-mode interrupt
+ * M/S/U        2     00  U-mode interrupt
+ * M/S/U        2     01  S-mode interrupt
+ * M/S/U        2     10  Reserved
+ * M/S/U        2     11  M-mode interrupt
+ *
+ * M/S/U        3     xx  Reserved
+ */
+static uint8_t riscv_clic_effective_mode(RISCVCLICState *clic, uint8_t intattr)
+{
+    uint8_t mode = get_field(intattr, CLIC_INTATTR_MODE);
+
+    switch (clic->nmbits) {
+    case 0:
+        mode = PRV_M;
+        break;
+
+    case 1:
+        if (mode <= PRV_S) {
+            if (clic->prv_s) {
+                mode = PRV_S;
+            } else {
+                assert(clic->prv_u);
+                mode = PRV_U;
+            }
+        } else {
+            mode = PRV_M;
+        }
+        break;
+
+    case 2:
+        /* no modification required */
+        break;
+
+    default:
+        /* We validate nmbits so this shouldn't be possible */
+        assert(clic->nmbits <= 2);
+    }
+
+    return mode;
+}
+
+/* Return target interrupt number */
+static int riscv_clic_get_irq(RISCVCLICState *clic, hwaddr addr)
+{
+    return addr / 4;
+}
+
+/* Encode the priority and IRQ as a single sortable value */
+static inline int riscv_clic_encode_priority(const CLICActiveInterrupt *i)
+{
+    /* Highest mode+level+priority */
+    int priority = (i->intcfg & CLIC_INTCFG_MASK) << CLIC_IRQ_BITS;
+    /* Highest irq number */
+    int irq = i->irq & CLIC_IRQ_MASK;
+    /* Combined */
+    return priority | irq;
+}
+
+static int riscv_clic_active_compare(const void *a, const void *b)
+{
+    return riscv_clic_encode_priority(b) - riscv_clic_encode_priority(a);
+}
+
+static void
+riscv_clic_update_intie(RISCVCLICState *clic, int mode,
+                        int irq, uint64_t new_intie)
+{
+    CLICActiveInterrupt *active_list = clic->active_list;
+
+    uint8_t old_intie = clic->clicintie[irq];
+    clic->clicintie[irq] = !!new_intie;
+
+    /* Add to or remove from list of active interrupts */
+    if (new_intie && !old_intie) {
+        uint16_t intcfg = (mode << CLIC_INTCFG_MODE_SHIFT) |
+                           clic->clicintctl[irq];
+        active_list[clic->active_count].intcfg = intcfg;
+        active_list[clic->active_count].irq = irq;
+        clic->active_count++;
+    } else if (!new_intie && old_intie) {
+        CLICActiveInterrupt key = {
+            (mode << 8) | clic->clicintctl[irq], irq
+        };
+        CLICActiveInterrupt *result = bsearch(&key,
+                                              active_list, clic->active_count,
+                                              sizeof(CLICActiveInterrupt),
+                                              riscv_clic_active_compare);
+        assert(result);
+        size_t elem = result - active_list;
+        size_t sz = --clic->active_count - elem;
+        memmove(&result[0], &result[1], sz);
+    }
+
+    /* Sort list of active interrupts */
+    qsort(active_list, clic->active_count,
+          sizeof(CLICActiveInterrupt),
+          riscv_clic_active_compare);
+
+    riscv_clic_next_interrupt(clic);
+}
+
+static void
+riscv_clic_hart_write(RISCVCLICState *clic, hwaddr addr,
+                      uint64_t value, unsigned size,
+                      int mode, int irq)
+{
+    int req = extract32(addr, 0, 2);
+
+    /* visibility is checked in riscv_clic_write */
+
+    if (irq >= clic->num_sources) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "clic: invalid irq %u: 0x%" HWADDR_PRIx "\n", irq, addr);
+        return;
+    }
+
+    switch (req) {
+    case 0: /* clicintip[i] */
+        if (riscv_clic_validate_intip(clic, irq)) {
+            /*
+             * The actual pending bit is located at bit 0 (i.e., the
+             * least significant bit). In case future extensions expand the bit
+             * field, from FW perspective clicintip[i]=zero means no interrupt
+             * pending, and clicintip[i]!=0 (not just 1) indicates an
+             * interrupt is pending. (Section 3.4)
+             */
+            if (value != clic->clicintip[irq]) {
+                riscv_clic_update_intip(clic, irq, value);
+            }
+        }
+        /* Handle a 32-bit write */
+        if (size > 1) {
+            unsigned width = min(size, 4);
+            unsigned i;
+            for (i = 1; i < width; i++) {
+                uint64_t local_value = (value >> (i * 8)) & 0xFF;
+                riscv_clic_hart_write(clic, addr + i, local_value,
+                                      1, mode, irq);
+            }
+        }
+        break;
+
+    case 1: /* clicintie[i] */
+        if (clic->clicintie[irq] != value) {
+            riscv_clic_update_intie(clic, mode, irq, value);
+        }
+        break;
+
+    case 2: /* clicintattr[i] */
+        uint8_t field_mode = riscv_clic_effective_mode(clic, value);
+        if (PRV_RESERVED == field_mode) {
+            field_mode = get_field(clic->clicintattr[irq],
+                                   CLIC_INTATTR_MODE);
+        }
+        value = set_field(value, CLIC_INTATTR_MODE, field_mode);
+        if (riscv_clic_validate_intattr(clic, value)) {
+            if (clic->clicintattr[irq] != value) {
+                clic->clicintattr[irq] = value;
+                riscv_clic_next_interrupt(clic);
+            }
+        }
+        break;
+
+    case 3: /* clicintctl[i] */
+        if (value != clic->clicintctl[irq]) {
+            clic->clicintctl[irq] = value;
+            riscv_clic_next_interrupt(clic);
+        }
+        break;
+    }
+}
+
+static uint64_t
+riscv_clic_hart_read(RISCVCLICState *clic, hwaddr addr, unsigned size,
+                     int mode, int irq)
+{
+    int req = extract32(addr, 0, 2);
+    int i;
+
+    /* visibility is checked in riscv_clic_read */
+
+    if (irq >= clic->num_sources) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "clic: invalid irq %u: 0x%" HWADDR_PRIx "\n", irq, addr);
+        return 0;
+    }
+
+    switch (req) {
+    case 0: /* clicintip[i] */
+        uint64_t retval = clic->clicintip[irq];
+        if (size > 1) {
+            /* Handle a multi-part read */
+            for (i = 1; i < size; ++i) {
+                uint64_t subval =
+                    (riscv_clic_hart_read(clic, addr + i, 1, mode, irq) & 0xFF);
+                retval |= subval << (i * 8);
+            }
+        }
+        return retval;
+
+    case 1: /* clicintie[i] */
+        return clic->clicintie[irq];
+    case 2: /* clicintattr[i] */
+        /*
+         * clicintattr register layout
+         * Bits Field
+         * 7:6 mode
+         * 5:3 reserved (WPRI 0)
+         * 2:1 trig
+         * 0 shv
+         */
+        uint8_t intattr = clic->clicintattr[irq] & CLIC_INTATTR_MASK;
+        int field_mode = riscv_clic_effective_mode(clic, intattr);
+        intattr = set_field(intattr, CLIC_INTATTR_MODE, field_mode);
+        return intattr;
+
+    case 3: /* clicintctl[i] */
+        /*
+         * The implemented bits are kept left-justified in the most-significant
+         * bits of each 8-bit clicintctl[i] register, with the lower
+         * unimplemented bits treated as hardwired to 1.(Section 3.7)
+         */
+        return clic->clicintctl[irq] |
+               ((1 << (8 - clic->clicintctlbits)) - 1);
+    }
+
+    return 0;
+}
+
+static void
+riscv_clic_write(void *opaque, hwaddr addr, uint64_t value, unsigned size)
+{
+    RISCVCLICView *clicview = opaque;
+    RISCVCLICState *clic = clicview->clic;
+    CPUState *cpu = cpu_by_arch_id(clic->hartid);
+    CPURISCVState *env = cpu ? cpu_env(cpu) : NULL;
+    hwaddr clic_size = clic->clic_size;
+    int mode = clicview->mode, irq;
+    const char *current_mode_str = (PRV_M == env->priv) ? "PRV_M" :
+                                   (PRV_S == env->priv) ? "PRV_S" :
+                                   (PRV_U == env->priv) ? "PRV_U" :
+                                   "unknown";
+    const char *access_mode_str = (PRV_M == mode) ? "PRV_M" :
+                                  (PRV_S == mode) ? "PRV_S" :
+                                  (PRV_U == mode) ? "PRV_U" :
+                                  "unknown";
+
+    assert(addr < clic_size);
+
+    if (mode > env->priv) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "clic: invalid write to %s CLIC registers in %s mode\n",
+                      access_mode_str, current_mode_str);
+        return;
+    }
+
+    if (addr < CLIC_INTCTL_BASE) {
+        assert(addr % 4 == 0);
+        int index = addr / 4;
+        switch (index) {
+        case 0: /* cliccfg */
+            {
+                uint8_t mnlbits = extract32(value, 0, 4);
+                uint8_t nmbits = extract32(value, 4, 2);
+                uint8_t snlbits = extract32(value, 16, 4);
+                uint8_t unlbits = extract32(value, 24, 4);
+
+                /*
+                 * The 4-bit cliccfg.mnlbits WARL field.
+                 * Valid values are 0—8.
+                 */
+                if (mnlbits <= 8 && PRV_M == mode) {
+                    clic->mnlbits = mnlbits;
+                }
+                if (clic->prv_s && snlbits <= 8 && mode >= PRV_S) {
+                    clic->snlbits = snlbits;
+                }
+                if (clic->prv_u && unlbits <= 8) {
+                    clic->unlbits = unlbits;
+                }
+
+                /*
+                 * The nmbits field - the number of bits for the mode.
+                 * Valid values are given by implemented privileges.
+                 * This is only accessible in PRV_M.
+                 */
+                if (PRV_M == mode) {
+                    if (clic->prv_s && clic->prv_u) {
+                        if (nmbits <= 2) {
+                            clic->nmbits = nmbits;
+                        }
+                    } else if (clic->prv_s || clic->prv_u) {
+                        if (nmbits <= 1) {
+                            clic->nmbits = nmbits;
+                        }
+                    } else {
+                        if (nmbits == 0) {
+                            clic->nmbits = 0;
+                        }
+                    }
+                }
+
+                break;
+            }
+        case CLIC_INTTRIG_START ... CLIC_INTTRIG_END: /* clicinttrig */
+            {
+                uint32_t interrupt_number = value & CLIC_INTTRIG_IRQN;
+                if (interrupt_number <= clic->num_sources) {
+                    value &= CLIC_INTTRIG_MASK;
+                    clic->clicinttrig[index - CLIC_INTTRIG_START] = value;
+                    /* TODO: How does this cause the interrupt to trigger? */
+                }
+                break;
+            }
+        case 2: /* mintthresh - only in CLIC spec v0.8 */
+            if (0 == strcmp(clic->version, "v0.8")) {
+                clic->mintthresh = value;
+                break;
+            }
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "clic: invalid write addr: 0x%" HWADDR_PRIx "\n",
+                          addr);
+            break;
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "clic: invalid write addr: 0x%" HWADDR_PRIx "\n",
+                          addr);
+            return;
+        }
+    } else {
+        addr -= CLIC_INTCTL_BASE;
+        irq = riscv_clic_get_irq(clic, addr);
+
+        if (riscv_clic_check_visible(clic, mode, irq)) {
+            riscv_clic_hart_write(clic, addr, value, size, mode, irq);
+        }
+    }
+}
+
+static uint64_t
+riscv_clic_read(void *opaque, hwaddr addr, unsigned size)
+{
+    RISCVCLICView *clicview = opaque;
+    RISCVCLICState *clic = clicview->clic;
+    CPUState *cpu = cpu_by_arch_id(clic->hartid);
+    CPURISCVState *env = cpu ? cpu_env(cpu) : NULL;
+    hwaddr clic_size = clic->clic_size;
+    int mode = clicview->mode, irq;
+
+    assert(addr < clic_size);
+
+    if (mode > env->priv) {
+        const char *current_mode_str = (PRV_M == env->priv) ? "PRV_M" :
+                                       (PRV_S == env->priv) ? "PRV_S" :
+                                       (PRV_U == env->priv) ? "PRV_U" :
+                                       "unknown";
+        const char *access_mode_str = (PRV_M == mode) ? "PRV_M" :
+                                      (PRV_S == mode) ? "PRV_S" :
+                                      (PRV_U == mode) ? "PRV_U" :
+                                      "unknown";
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "clic: invalid write to %s CLIC registers in %s mode\n",
+                      access_mode_str, current_mode_str);
+        return 0;
+    }
+
+    if (addr < CLIC_INTCTL_BASE) {
+        assert(addr % 4 == 0);
+        int index = addr / 4;
+        switch (index) {
+        case 0:
+            /*
+             * cliccfg register layout
+             *
+             * Bits     Field
+             * 31:28    reserved (WPRI 0)
+             * 27:24    unlbits
+             * 23:20    reserved (WPRI 0)
+             * 19:16    snlbits
+             * 15:6     reserved (WPRI 0)
+             *  5:4     nmbits
+             *  3:0     mnlbits
+             */
+            uint64_t cliccfg = 0;
+            if (PRV_M == mode) {
+                cliccfg = clic->mnlbits | (clic->nmbits << 4);
+            }
+            if (clic->prv_s && mode >= PRV_S) {
+                cliccfg |= clic->snlbits << 16;
+            }
+            if (clic->prv_u && mode >= PRV_U) {
+                cliccfg |= clic->unlbits << 24;
+            }
+            return cliccfg;
+
+        case CLIC_INTTRIG_START ... CLIC_INTTRIG_END: /* clicinttrig */
+            /*
+             * clicinttrig register layout
+             *
+             * Bits Field
+             * 31 enable
+             * 30:13 reserved (WARL 0)
+             * 12:0 interrupt_number
+             */
+            uint64_t inttrig = clic->clicinttrig[index - CLIC_INTTRIG_START];
+            return inttrig & CLIC_INTTRIG_MASK;
+
+        case 2: /* mintthresh - only in CLIC spec v0.8 */
+            if (0 == strcmp(clic->version, "v0.8")) {
+                return clic->mintthresh;
+                break;
+            }
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "clic: invalid read : 0x%" HWADDR_PRIx "\n",
+                          addr);
+            break;
+
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "clic: invalid read : 0x%" HWADDR_PRIx "\n",
+                          addr);
+            break;
+        }
+    } else {
+        addr -= CLIC_INTCTL_BASE;
+        irq = riscv_clic_get_irq(clic, addr);
+
+        if (riscv_clic_check_visible(clic, mode, irq)) {
+            return riscv_clic_hart_read(clic, addr, size, mode, irq);
+        }
+    }
+
+    return 0;
+}
+
+static void riscv_clic_set_irq(void *opaque, int id, int level)
+{
+    RISCVCLICState *clic = opaque;
+    TRIG_TYPE type;
+
+    type = riscv_clic_get_trigger_type(clic, id);
+
+    /*
+     * In general, the edge-triggered interrupt state should be kept in pending
+     * bit, while the level-triggered interrupt should be kept in the level
+     * state of the incoming wire.
+     *
+     * For CLIC, model the level-triggered interrupt by read-only pending bit.
+     */
+    if (level) {
+        switch (type) {
+        case POSITIVE_LEVEL:
+        case POSITIVE_EDGE:
+            riscv_clic_update_intip(clic, id, level);
+            break;
+        case NEG_LEVEL:
+            riscv_clic_update_intip(clic, id, !level);
+            break;
+        case NEG_EDGE:
+            break;
+        default:
+            /* It's a 2-bit field so this shouldn't be possible */
+            assert(type <= 3);
+        }
+    } else {
+        switch (type) {
+        case POSITIVE_LEVEL:
+            riscv_clic_update_intip(clic, id, level);
+            break;
+        case POSITIVE_EDGE:
+            break;
+        case NEG_LEVEL:
+        case NEG_EDGE:
+            riscv_clic_update_intip(clic, id, !level);
+            break;
+        default:
+            /* It's a 2-bit field so this shouldn't be possible */
+            assert(type <= 3);
+        }
+    }
+}
+
+static void riscv_clic_cpu_irq_handler(void *opaque, int irq, int level)
+{
+    CPURISCVState *env = (CPURISCVState *)opaque;
+    RISCVCLICState *clic = env->clic;
+
+    if (level) {
+        env->exccode = clic->exccode;
+        cpu_interrupt(env_cpu(env), CPU_INTERRUPT_CLIC);
+    }
+}
+
+static const MemoryRegionOps riscv_clic_ops = {
+    .read = riscv_clic_read,
+    .write = riscv_clic_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 8
+    }
+};
+
+static void riscv_clic_realize(DeviceState *dev, Error **errp)
+{
+    RISCVCLICState *clic = RISCV_CLIC(dev);
+    size_t irqs = clic->num_sources;
+
+    if (clic->prv_s && clic->prv_u) {
+        clic->nmbits = 2;
+    } else if (clic->prv_s || clic->prv_u) {
+        clic->nmbits = 1;
+    } else {
+        clic->nmbits = 0;
+    }
+
+    clic->clicintip = g_new0(uint8_t, irqs);
+    clic->clicintie = g_new0(uint8_t, irqs);
+    clic->clicintattr = g_new0(uint8_t, irqs);
+    clic->clicintctl = g_new0(uint8_t, irqs);
+    clic->active_list = g_new0(CLICActiveInterrupt, irqs);
+
+    if (!clic->prv_s) {
+        clic->snlbits = 0;
+    }
+    if (!clic->prv_u) {
+        clic->unlbits = 0;
+    }
+
+    /* Allocate irqs through gpio, so that we can use qtest */
+    qdev_init_gpio_in(dev, riscv_clic_set_irq, irqs);
+    qdev_init_gpio_out(dev, &clic->cpu_irq, 1);
+
+    assert(cpu_exists(clic->hartid));
+    RISCVCPU *cpu = RISCV_CPU(qemu_get_cpu(clic->hartid));
+    qemu_irq irq = qemu_allocate_irq(riscv_clic_cpu_irq_handler, &cpu->env, 1);
+    qdev_connect_gpio_out(dev, 0, irq);
+    cpu->env.clic = clic;
+}
+
+static void riscv_clic_view_realize(DeviceState *dev, Error **errp)
+{
+    RISCVCLICView *clicview = RISCV_CLIC_VIEW(dev);
+    RISCVCLICState *clic = clicview->clic;
+
+    memory_region_init_io(&clicview->mmio, OBJECT(clicview), &riscv_clic_ops,
+                          clicview, TYPE_RISCV_CLIC_VIEW, clic->clic_size);
+    sysbus_init_mmio(SYS_BUS_DEVICE(clicview), &clicview->mmio);
+}
+
+static Property riscv_clic_properties[] = {
+    DEFINE_PROP_BOOL("shv-enabled", RISCVCLICState, shv_enabled, true),
+    DEFINE_PROP_BOOL("jump-table", RISCVCLICState, jump_table, false),
+    DEFINE_PROP_UINT8("mnlbits", RISCVCLICState, mnlbits, 8),
+    DEFINE_PROP_UINT8("snlbits", RISCVCLICState, snlbits, 8),
+    DEFINE_PROP_UINT8("unlbits", RISCVCLICState, unlbits, 8),
+    DEFINE_PROP_INT32("hartid", RISCVCLICState, hartid, 0),
+    DEFINE_PROP_UINT32("num-sources", RISCVCLICState, num_sources, 0),
+    DEFINE_PROP_UINT32("clic-size", RISCVCLICState, clic_size, 0),
+    DEFINE_PROP_UINT32("clicintctlbits", RISCVCLICState, clicintctlbits, 0),
+    DEFINE_PROP_STRING("version", RISCVCLICState, version),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static Property riscv_clic_view_properties[] = {
+    DEFINE_PROP_LINK("clic", RISCVCLICView, clic,
+                     TYPE_RISCV_CLIC, RISCVCLICState *),
+    DEFINE_PROP_UINT8("mode", RISCVCLICView, mode, PRV_U),
+    DEFINE_PROP_UINT64("clicbase", RISCVCLICView, clicbase, 0),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static void riscv_clic_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = riscv_clic_realize;
+    device_class_set_props(dc, riscv_clic_properties);
+}
+
+static void riscv_clic_view_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = riscv_clic_view_realize;
+    device_class_set_props(dc, riscv_clic_view_properties);
+}
+
+static const TypeInfo riscv_clic_info = {
+    .name          = TYPE_RISCV_CLIC,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(RISCVCLICState),
+    .class_init    = riscv_clic_class_init,
+};
+
+static const TypeInfo riscv_clic_view_info = {
+    .name = TYPE_RISCV_CLIC_VIEW,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(RISCVCLICView),
+    .class_init = riscv_clic_view_init,
+};
+
+static void riscv_clic_register_types(void)
+{
+    type_register_static(&riscv_clic_info);
+    type_register_static(&riscv_clic_view_info);
+}
+
+type_init(riscv_clic_register_types)
+
+/*
+ * riscv_clic_view_create:
+ *
+ * @clic: machine-mode CLIC this is an view onto
+ * @clicbase: base address of this view CLIC memory-mapped registers
+ * @mode: the mode of the view - PRV_S or PRV_U
+ *
+ * Returns: the new view
+ */
+static RISCVCLICView *riscv_clic_view_create(RISCVCLICState *clic,
+                                             hwaddr clicbase, int mode)
+{
+    DeviceState *dev = qdev_new(TYPE_RISCV_CLIC_VIEW);
+    RISCVCLICView *clicview = RISCV_CLIC_VIEW(dev);
+    Object *obj = OBJECT(dev);
+    Object *clicobj = OBJECT(clic);
+
+    assert(0 != clic);                  /* this should exist */
+    assert(0 != clicbase);              /* this should exist */
+    assert(0 == (clicbase & 0xfff));    /* base should be 4KiB-aligned */
+    assert(PRV_M == mode || PRV_S == mode || PRV_U == mode);
+
+    object_property_add_child(clicobj, modeview_name[mode], obj);
+    clicview->clic = clic;
+
+    qdev_prop_set_uint8(dev, "mode", mode);
+    qdev_prop_set_uint64(dev, "clicbase", clicbase);
+
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal)) {
+        object_unparent(obj);
+        return NULL;
+    }
+
+    memory_region_init_io(&clicview->mmio, OBJECT(dev), &riscv_clic_ops,
+                          clicview, TYPE_RISCV_CLIC_VIEW, clic->clic_size);
+    sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, clicbase);
+
+    return clicview;
+}
+
+/*
+ * riscv_clic_create:
+ *
+ * @mclicbase: base address of PRV_M CLIC memory-mapped registers
+ * @sclicbase: base address of PRV_S CLIC memory-mapped registers
+ * @uclicbase: base address of PRV_U CLIC memory-mapped registers
+ * @hartid: the HART ID this CLIC is serving
+ * @num_sources: number of interrupts supporting by each aperture
+ * @clicintctlbits: bits are actually implemented in the clicintctl registers
+ * @version: clic version, such as "v0.9"; append "-jmp" for jump table instead
+ *           of function pointers
+ *
+ * Returns: the device object
+ */
+DeviceState *riscv_clic_create(hwaddr mclicbase, hwaddr sclicbase,
+                               hwaddr uclicbase, uint32_t hartid,
+                               uint32_t num_sources, uint8_t clicintctlbits,
+                               const char *version)
+{
+    DeviceState *dev = qdev_new(TYPE_RISCV_CLIC);
+    RISCVCLICState *s = RISCV_CLIC(dev);
+    g_autofree char **tokens = NULL;
+    char *base_version;
+    bool jump_table = false;
+
+    assert(num_sources <= CLIC_MAX_IRQ_COUNT);
+    assert(cpu_exists(hartid));
+    assert(clicintctlbits <= MAX_CLIC_INTCTLBITS);
+    assert(0 == (mclicbase & 0xfff));    /* base should be 4KiB-aligned */
+
+    /* Parse the version */
+    tokens = g_strsplit(version, "-", 2);
+    base_version = g_strdup(tokens[0]);
+    assert(0 == strcmp(base_version, "v0.9"));
+    if (tokens[1]) {
+        assert(0 == strcmp(tokens[1], "jmp"));
+        jump_table = true;
+    }
+
+    qdev_prop_set_uint32(dev, "hartid", hartid);
+    qdev_prop_set_uint32(dev, "num-sources", num_sources);
+    qdev_prop_set_uint32(dev, "clic-size", num_sources * 4 + CLIC_INTCTL_BASE);
+    qdev_prop_set_uint32(dev, "clicintctlbits", clicintctlbits);
+    qdev_prop_set_string(dev, "version", base_version);
+    qdev_prop_set_bit(dev, "jump-table", jump_table);
+
+    s->prv_m = riscv_clic_view_create(s, mclicbase, PRV_M);
+    if (sclicbase) {
+        s->prv_s = riscv_clic_view_create(s, sclicbase, PRV_S);
+    }
+    if (uclicbase) {
+        s->prv_u = riscv_clic_view_create(s, uclicbase, PRV_U);
+    }
+
+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);
+    return dev;
+}
+
+void riscv_clic_get_next_interrupt(void *opaque)
+{
+    RISCVCLICState *clic = opaque;
+    riscv_clic_next_interrupt(clic);
+}
+
+bool riscv_clic_shv_interrupt(void *opaque, int irq)
+{
+    RISCVCLICState *clic = opaque;
+    return riscv_clic_is_shv_interrupt(clic, irq);
+}
+
+bool riscv_clic_edge_triggered(void *opaque, int irq)
+{
+    RISCVCLICState *clic = opaque;
+    return riscv_clic_is_edge_triggered(clic, irq);
+}
+
+bool riscv_clic_use_jump_table(void *opaque)
+{
+    RISCVCLICState *clic = opaque;
+    return clic->jump_table;
+}
+
+void riscv_clic_clean_pending(void *opaque, int irq)
+{
+    RISCVCLICState *clic = opaque;
+    clic->clicintip[irq] = 0;
+}
+
+/*
+ * The new CLIC interrupt-handling mode is encoded as a new state in
+ * the existing WARL xtvec register, where the low two bits are 11.
+ */
+bool riscv_clic_is_clic_mode(CPURISCVState *env)
+{
+    target_ulong xtvec = (env->priv == PRV_M) ? env->mtvec : env->stvec;
+    return env->clic && ((xtvec & XTVEC_MODE) == XTVEC_CLIC);
+}
+
+void riscv_clic_decode_exccode(uint32_t exccode, int *mode,
+                               int *il, int *irq)
+{
+    *irq = get_field(exccode, RISCV_EXCP_CLIC_IRQ);
+    *mode = get_field(exccode, RISCV_EXCP_CLIC_MODE);
+    *il = get_field(exccode, RISCV_EXCP_CLIC_LEVEL);
+}
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index a2030e3a6f..18454df8bd 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -47,6 +47,7 @@ config RISCV_VIRT
     select SERIAL
     select RISCV_ACLINT
     select RISCV_APLIC
+    select RISCV_CLIC
     select RISCV_IMSIC
     select SIFIVE_PLIC
     select SIFIVE_TEST
diff --git a/include/hw/intc/riscv_clic.h b/include/hw/intc/riscv_clic.h
new file mode 100644
index 0000000000..b38a576308
--- /dev/null
+++ b/include/hw/intc/riscv_clic.h
@@ -0,0 +1,213 @@
+/*
+ * RISC-V CLIC(Core Local Interrupt Controller) interface.
+ *
+ * Copyright (c) 2021 T-Head Semiconductor Co., Ltd. All rights reserved.
+ * Copyright (c) 2024 Cirrus Logic, Inc
+ *      and Cirrus Logic International Semiconductor Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2 or later, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * This implementation follows the CLIC 0.9-stable draft at 14 March 2024,
+ * with the following exceptions and implementation details:
+ *  - the CLIC control registers are memory-mapped as per earlier drafts (in
+ *    particular version 0.9-draft, 20 June 2023)
+ *  - the indirect CSR control in 0.9-stable is not implemented
+ *  - the vector table can be either handler addresses (as per the spec)
+      or a jump table where each entry is processed as an instruction,
+      selectable with version number v0.9-jmp
+ *  - each hart is assigned its own CLIC block
+ *  - if PRV_S and/or PRV_M are supported, they are currently assumed to follow
+ *    the PRV_M registers; a subsequent update will address this
+ *  - support for PRV_S and PRV_M is selectable at CLIC instantiation
+ *  - PRV_S and PRV_U registers are currently separate from PRV_M; a subsequent
+ *    update will turn them into filtered views onto the PRV_M registers
+ *  - each hart is assigned its own CLIC block
+ *  - support for PRV_S and PRV_M is selectable at CLIC instantiation by
+ *    passing in a base address for the given modes; a base address of 0 is
+ *    treated as not supported
+ *  - PRV_S and PRV_U registers are mapped  onto the PRV_M controls with
+ *    appropriate filtering for the access mode
+ *
+ * The implementation has a RISCVCLICState per hart, with a RISCVCLICView
+ * for each mode subsidiary to that. Each view knows its access mode and base
+ * address, as well as the RISCVCLICState with which it is associated.
+ *
+ * MMIO accesses go through the view, allowing the appropriate permissions to
+ * be enforced when accessing the parent RISCVCLICState for the settings.
+ */
+
+#ifndef RISCV_CLIC_H
+#define RISCV_CLIC_H
+
+#include "hw/irq.h"
+#include "hw/sysbus.h"
+
+#define TYPE_RISCV_CLIC "riscv_clic"
+#define TYPE_RISCV_CLIC_VIEW "riscv_clic_view"
+#define RISCV_CLIC(obj) \
+    OBJECT_CHECK(RISCVCLICState, (obj), TYPE_RISCV_CLIC)
+#define RISCV_CLIC_VIEW(obj) \
+    OBJECT_CHECK(RISCVCLICView, (obj), TYPE_RISCV_CLIC_VIEW)
+
+/*
+ * CLIC per hart active interrupts
+ *
+ * We maintain per hart lists of enabled interrupts sorted by
+ * mode+level+priority. The sorting is done on the configuration path
+ * so that the interrupt delivery fastpath can linear scan enabled
+ * interrupts in priority order.
+ */
+typedef struct CLICActiveInterrupt {
+    uint16_t intcfg;
+    uint16_t irq;
+} CLICActiveInterrupt;
+
+typedef enum TRIG_TYPE {
+    POSITIVE_LEVEL,
+    POSITIVE_EDGE,
+    NEG_LEVEL,
+    NEG_EDGE,
+} TRIG_TYPE;
+
+#define CLIC_INTCTL_BASE        0x1000  /* start offset of intctl registers */
+#define MAX_CLIC_INTCTLBITS     8       /* maximum value for intctlbits */
+
+/* maximum of 4096 IRQs */
+#define CLIC_IRQ_BITS           12
+#define CLIC_MAX_IRQ_COUNT      (1 << CLIC_IRQ_BITS)
+#define CLIC_MAX_IRQ            (CLIC_MAX_IRQ_COUNT - 1)
+#define CLIC_IRQ_MASK           CLIC_MAX_IRQ
+
+/*
+ * clicinttrig registers
+ * 31       interrupt_trap_enable
+ * 30       nxti_enable
+ * 29:13    reserved (WARL 0)
+ * 12:0     interrupt_number
+ */
+#define CLIC_INTTRIG_REGS       32      /* inttrig register count */
+#define CLIC_INTTRIG_START      0x10    /* first inttrig register */
+#define CLIC_INTTRIG_END        (CLIC_INTTRIG_START + CLIC_INTTRIG_REGS - 1)
+#define CLIC_INTTRIG_TRAP_ENA   0x80000000
+#define CLIC_INTTRIG_NXTI_ENA   0x40000000
+#define CLIC_INTTRIG_IRQN       0x00001fff
+#define CLIC_INTTRIG_MASK       (CLIC_INTTRIG_TRAP_ENA | \
+                                 CLIC_INTTRIG_NXTI_ENA | CLIC_INTTRIG_IRQN)
+
+/*
+ * We combine the mode and intctl to a number so that higher modes come first.
+ * 9:8  machine mode
+ * 7:0  clicintctl
+ */
+#define CLIC_INTCFG_MODE_SHIFT  8
+#define CLIC_INTCFG_MODE        0x300
+#define CLIC_INTCFG_CTL         0xff
+#define CLIC_INTCFG_MASK        (CLIC_INTCFG_MODE | CLIC_INTCFG_CTL)
+
+/*
+ * clicintattr layout
+ * 7:6  mode
+ * 5:3  reserved (WPRI 0)
+ * 2:1  trig
+ * 0    shv
+ */
+#define CLIC_INTATTR_MODE_SHIFT     6
+#define CLIC_INTATTR_MODE_WIDTH     2
+#define CLIC_INTATTR_MODE           0xc0
+#define CLIC_INTATTR_TRIG_SHIFT     1
+#define CLIC_INTATTR_TRIG_WIDTH     2
+#define CLIC_INTATTR_TRIG           0x06
+#define CLIC_INTATTR_SHV            0x01
+#define CLIC_INTATTR_MASK           (CLIC_INTATTR_MODE | CLIC_INTATTR_TRIG | \
+                                     CLIC_INTATTR_SHV)
+/* The clicintattr value */
+#define CLIC_INTATTR_TRIG_EDGE      0b01    /* trig decode edge-triggered */
+#define CLIC_INTATTR_TRIG_INV       0b10    /* trig decode negative polarity */
+
+/* Forward declaration */
+typedef struct RISCVCLICView RISCVCLICView;
+
+/*
+ * The main CLIC state (PRV_M mode) for a hart.
+ */
+typedef struct RISCVCLICState {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+
+    /* Implementation parameters */
+    bool shv_enabled;       /* hardware-vectoring enabled */
+    bool jump_table;        /* vector with jump table, not handler addresses */
+    int hartid;
+    uint32_t num_sources;
+    uint32_t clic_size;
+    uint32_t clic_mmode_base;
+    uint32_t clicintctlbits;
+    RISCVCLICView *prv_m;    /* our PRV_M view */
+    RISCVCLICView *prv_s;    /* our PRV_S view */
+    RISCVCLICView *prv_u;    /* our PRV_U view */
+    char *version;
+
+    /* Global configuration */
+    uint8_t nmbits;         /* mode bits */
+    uint8_t mnlbits;        /* level bits for M-mode */
+    uint8_t snlbits;        /* level bits for S-mode, if present */
+    uint8_t unlbits;        /* level bits for U-mode, if present */
+    uint32_t clicinttrig[CLIC_INTTRIG_REGS];
+
+    /* Aperture configuration */
+    uint8_t *clicintip;
+    uint8_t *clicintie;
+    uint8_t *clicintattr;
+    uint8_t *clicintctl;
+
+    /* Compatible with v0.8 */
+    uint32_t mintthresh;
+    uint32_t sintthresh;
+    uint32_t uintthresh;
+
+    /* QEMU implementation related fields */
+    uint32_t exccode;
+    CLICActiveInterrupt *active_list;
+    size_t active_count;
+    qemu_irq cpu_irq;
+} RISCVCLICState;
+
+/*
+ * A PRV_S or PRV_U overlay onto the main RISCVCLICState.
+ */
+typedef struct RISCVCLICView {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    RISCVCLICState *clic;       /* the CLIC this is a view onto */
+    MemoryRegion mmio;
+    uint64_t clicbase;
+    uint8_t mode;
+} RISCVCLICView;
+
+DeviceState *riscv_clic_create(hwaddr mclicbase, hwaddr sclicbase,
+                               hwaddr uclicbase, uint32_t hartid,
+                               uint32_t num_sources, uint8_t clicintctlbits,
+                               const char *version);
+
+void riscv_clic_decode_exccode(uint32_t exccode, int *mode, int *il, int *irq);
+void riscv_clic_clean_pending(void *opaque, int irq);
+bool riscv_clic_edge_triggered(void *opaque, int irq);
+bool riscv_clic_shv_interrupt(void *opaque, int irq);
+bool riscv_clic_use_jump_table(void *opaque);
+void riscv_clic_get_next_interrupt(void *opaque);
+bool riscv_clic_is_clic_mode(CPURISCVState *env);
+#endif
diff --git a/target/riscv/cpu.h b/target/riscv/cpu.h
index 9b5f36ad0a..12aa8cf6b1 100644
--- a/target/riscv/cpu.h
+++ b/target/riscv/cpu.h
@@ -36,6 +36,7 @@
 typedef struct CPUArchState CPURISCVState;

 #define CPU_RESOLVING_TYPE TYPE_RISCV_CPU
+#define CPU_INTERRUPT_CLIC CPU_INTERRUPT_TGT_EXT_0

 #if defined(TARGET_RISCV32)
 # define TYPE_RISCV_CPU_BASE            TYPE_RISCV_CPU_BASE32
@@ -465,6 +466,7 @@ struct CPUArchState {
     bool vstime_irq;

     void *clic;       /* clic interrupt controller */
+    uint32_t exccode; /* clic irq encode */

     hwaddr kernel_addr;
     hwaddr fdt_addr;
diff --git a/target/riscv/cpu_bits.h b/target/riscv/cpu_bits.h
index ad45402370..0ed44ec0a8 100644
--- a/target/riscv/cpu_bits.h
+++ b/target/riscv/cpu_bits.h
@@ -694,6 +694,12 @@ typedef enum RISCVException {
 } RISCVException;

 #define RISCV_EXCP_INT_FLAG                0x80000000
+#define RISCV_EXCP_CLIC                    0x40000000
+#define RISCV_EXCP_CLIC_LEVEL_SHIFT        14
+#define RISCV_EXCP_CLIC_LEVEL              (0xff << RISCV_EXCP_CLIC_LEVEL_SHIFT)
+#define RISCV_EXCP_CLIC_MODE_SHIFT         12
+#define RISCV_EXCP_CLIC_MODE               (3 << RISCV_EXCP_CLIC_MODE_SHIFT)
+#define RISCV_EXCP_CLIC_IRQ                0x00000fff
 #define RISCV_EXCP_INT_MASK                0x7fffffff

 /* Interrupt causes */
@@ -746,6 +752,17 @@ typedef enum RISCVException {
 #define SINTSTATUS_SIL                     0x0000ff00 /* sil[15:8] */
 #define SINTSTATUS_UIL                     0x000000ff /* uil[7:0] */

+/* mtvec & stvec */
+#define XTVEC_MODE                         0x03
+#define XTVEC_SUBMODE                      0x3c
+#define XTVEC_FULL_MODE                    (XTVEC_MODE | XTVEC_SUBMODE)
+#define XTVEC_OBASE                        (~XTVEC_MODE)
+#define XTVEC_NBASE                        (~XTVEC_FULL_MODE)
+
+#define XTVEC_CLINT_DIRECT                 0x0
+#define XTVEC_CLINT_VECTORED               0x1
+#define XTVEC_CLIC                         0x3
+
 /* MIE masks */
 #define MIE_SEIE                           (1 << IRQ_S_EXT)
 #define MIE_UEIE                           (1 << IRQ_U_EXT)
