hw/riscv: fix interrupts being lost or delayed when MIE=0 on the ESP32-C3

This commit is contained in:
Omar Chebib
2026-04-17 09:52:17 +08:00
parent 00b2b7500e
commit 40edccac41
4 changed files with 78 additions and 240 deletions
+41 -156
View File
@@ -30,12 +30,6 @@
#define SET_BIT(reg, bit) do { (reg) |= BIT(bit); } while(0)
static void esp32c3_do_int(ESP32C3IntMatrixState *s, int line)
{
qemu_irq_pulse(s->out_irqs[line]);
}
static int esp32c3_get_output_line_level(ESP32C3IntMatrixState *s, int line)
{
int level_shared = 0;
@@ -51,27 +45,37 @@ static int esp32c3_get_output_line_level(ESP32C3IntMatrixState *s, int line)
return level_shared;
}
/**
* Try to clear the given interrupt from the pending bitmap.
* If the signal is shared with other interrupt sources, make sure all of them are low (0)
* before clearing the pending IRQ from the bitmap.
*/
static void esp32c3_intmatrix_clear_pending(ESP32C3IntMatrixState *s, int line)
static bool esp32c3_intmatrix_line_should_assert(ESP32C3IntMatrixState *s, int line)
{
/* Check if another GPIO IRQ is sharing the same output line. They must all be low before
* clearing the pending bit. This is due to the fact that output lines
* can be shared on the ESP32-C3 */
const int output_level = esp32c3_get_output_line_level(s, line);
if (!output_level) {
/* The output interrupt line is 0, so we can clear the pending flag */
CLEAR_BIT(s->irq_pending, line);
if (line == 0) {
return false;
}
return BIT_SET(s->irq_enabled, line) &&
esp32c3_get_output_line_level(s, line) != 0 &&
(s->irq_prio[line] >= s->irq_thres);
}
static void esp32c3_intmatrix_update_line(ESP32C3IntMatrixState *s, int line)
{
if (line == 0) {
return;
}
const bool assert_line = esp32c3_intmatrix_line_should_assert(s, line);
if (assert_line) {
qemu_irq_raise(s->out_irqs[line]);
} else {
qemu_irq_lower(s->out_irqs[line]);
}
}
static inline bool esp32c3_intmatrix_can_trigger(ESP32C3IntMatrixState *s)
static void esp32c3_intmatrix_refresh_all(ESP32C3IntMatrixState *s)
{
return esp_cpu_accept_interrupts(s->cpu);
for (uint32_t i = 1; i <= ESP32C3_CPU_INT_COUNT; i++) {
esp32c3_intmatrix_update_line(s, i);
}
}
@@ -80,7 +84,7 @@ static void esp32c3_intmatrix_irq_handler(void *opaque, int n, int level)
ESP32C3IntMatrixState *s = ESP32C3_INTMATRIX(opaque);
/* Update the level mirror */
assert(n <= ESP32C3_INT_MATRIX_INPUTS);
assert(n < ESP32C3_INT_MATRIX_INPUTS);
/* Make sure level is 0 or 1 */
level = level ? 1 : 0;
@@ -94,128 +98,13 @@ static void esp32c3_intmatrix_irq_handler(void *opaque, int n, int level)
CLEAR_BIT(s->irq_levels, n);
}
const int line = s->irq_map[n];
/* If the line is not enable, don't do anything special, the level has been recorded already.
* Don't do anything if the line is at the same level as before */
if ((s->irq_enabled & BIT(line)) == 0 || former_level == level) {
return;
}
/* If the new level is high, check that the priority is equal or bigger than the threshold.
* If that's the case, we can execute the interrupt, else, mark it as pending. */
if (level == 1) {
#if INTMATRIX_DEBUG
info_report("\x1b[31m[INTMATRIX] IRQ %d priority set to %d, CPU threshold %d \x1b[0m\n",
line, s->irq_prio[line], s->irq_thres);
#endif
if (s->irq_prio[line] >= s->irq_thres && esp32c3_intmatrix_can_trigger(s)) {
esp32c3_do_int(s, line);
} else {
SET_BIT(s->irq_pending, line);
}
} else if (BIT_SET(s->irq_pending, line)) {
esp32c3_intmatrix_clear_pending(s, line);
/* Nothing to do if the level is unchanged. */
if (former_level != level) {
const int line = s->irq_map[n];
esp32c3_intmatrix_update_line(s, line);
}
}
static void esp32c3_intmatrix_irq_prio_changed(ESP32C3IntMatrixState* s, uint32_t line, uint8_t priority)
{
const bool accept = esp32c3_intmatrix_can_trigger(s);
if (accept && priority >= s->irq_thres && BIT_SET(s->irq_pending, line)) {
/* No need to clear the pending bit here. As soon as the interrupt source will be ACK by the
* software, its level will be update, as well as its pending state. */
esp32c3_do_int(s, line);
}
}
static void esp32c3_intmatrix_core_prio_changed(ESP32C3IntMatrixState* s, uint64_t new_cpu_priority)
{
uint64_t pending = s->irq_pending;
const bool accept = esp32c3_intmatrix_can_trigger(s);
if (pending && accept) {
int64_t priority = -1;
uint_fast32_t line = 0;
/* Clear all the interrupts that have a lower priority than the new CPU threshold */
for (uint_fast32_t i = 1; i <= ESP32C3_CPU_INT_COUNT; i++) {
const uint64_t line_prio = s->irq_prio[i];
if (line_prio < new_cpu_priority) {
CLEAR_BIT(pending, i);
}
}
/* No high level interrupt pending? */
if (pending == 0) {
return;
}
/* Look for the highest priority pending interrupt */
for (uint_fast32_t i = 1; i <= ESP32C3_CPU_INT_COUNT; i++) {
const int64_t line_prio = (int64_t) s->irq_prio[i];
if (BIT_SET(pending, i) && line_prio > priority) {
priority = line_prio;
line = i;
}
}
/* Make sure a line was selected with its new priority */
assert(line != 0);
assert(priority >= new_cpu_priority);
/* No need to clear the pending bit here. As soon as the interrupt source will be ACK by the
* software, its level will be update, as well as its pending state. */
esp32c3_do_int(s, line);
}
}
/**
* This function is called when the status (enabled/disabled) of a line has just been changed.
* It will update the pending IRQ map.
*/
static void esp32c3_intmatrix_irq_status_changed(ESP32C3IntMatrixState* s, uint32_t line, int enabled)
{
const bool accept = esp32c3_intmatrix_can_trigger(s);
if (!enabled) {
/* IRQ has just been disabled, if any interrupt is pending, clear it */
CLEAR_BIT(s->irq_pending, line);
} else if (esp32c3_get_output_line_level(s, line)) {
/* IRQ has just been re-enabled, we have to check if any interrupt source is mapped to it, and
* if that's the case, check if their level is high, as we would need to potentially trigger an
* interrupt. */
SET_BIT(s->irq_pending, line);
if (accept) {
/* If the CPU can accept interrupt, trigger an interrupt now */
esp32c3_do_int(s, line);
}
}
}
/**
* Callback invoked by the CPU as soon as interrupts are re-enabled
*/
static bool esp32c3_intmatrix_mie_enabled(void* opaque)
{
ESP32C3IntMatrixState *s = ESP32C3_INTMATRIX(opaque);
/* We need to check if any interrupt is pending and trigger it. We have such function already, triggered when
* the core priority changes, let's reuse this function by giving the same core priority */
esp32c3_intmatrix_core_prio_changed(s, s->irq_thres);
return s->irq_pending != 0;
}
static uint64_t esp32c3_intmatrix_read(void* opaque, hwaddr addr, unsigned int size)
{
ESP32C3IntMatrixState *s = ESP32C3_INTMATRIX(opaque);
@@ -251,11 +140,16 @@ static void esp32c3_intmatrix_write(void* opaque, hwaddr addr, uint64_t value, u
const uint32_t index = addr / sizeof(uint32_t);
if (index < ESP32C3_INT_MATRIX_INPUTS) {
s->irq_map[index] = (value & 0x1f);
const uint8_t old_line = s->irq_map[index];
const uint8_t new_line = (value & 0x1f);
s->irq_map[index] = new_line;
#if INTMATRIX_DEBUG
info_report("\x1b[31m[INTMATRIX] Mapping interrupt %d to CPU line %d\x1b[0m\n", index, s->irq_map[index]);
#endif
if (old_line != new_line) {
esp32c3_intmatrix_update_line(s, old_line);
esp32c3_intmatrix_update_line(s, new_line);
}
} else if (index >= ESP32C3_INTMATRIX_IO_PRIO_START && index < ESP32C3_INTMATRIX_IO_PRIO_END) {
@@ -266,7 +160,7 @@ static void esp32c3_intmatrix_write(void* opaque, hwaddr addr, uint64_t value, u
info_report("\x1b[31m[INTMATRIX] Priority of line %d set to %d\x1b[0m\n", line, priority);
#endif
/* Check if the new priority interrupts the CPU */
esp32c3_intmatrix_irq_prio_changed(s, line, priority);
esp32c3_intmatrix_update_line(s, line);
} else if (index == ESP32C3_INTMATRIX_IO_THRESH_REG) {
@@ -289,7 +183,7 @@ static void esp32c3_intmatrix_write(void* opaque, hwaddr addr, uint64_t value, u
#if INTMATRIX_DEBUG
info_report("\x1b[31m[INTMATRIX] Setting CPU IRQ threshold to %d\x1b[0m", priority);
#endif
esp32c3_intmatrix_core_prio_changed(s, priority);
esp32c3_intmatrix_refresh_all(s);
}
} else if (index == ESP32C3_INTMATRIX_IO_ENABLE_REG) {
@@ -302,7 +196,7 @@ static void esp32c3_intmatrix_write(void* opaque, hwaddr addr, uint64_t value, u
const int new_st = value & BIT(i);
const int old_st = prev & BIT(i);
if (new_st != old_st) {
esp32c3_intmatrix_irq_status_changed(s, i, new_st ? 1 : 0);
esp32c3_intmatrix_update_line(s, i);
}
}
} else if (index == ESP32C3_INTMATRIX_IO_TYPE_REG) {
@@ -334,7 +228,6 @@ static void esp32c3_intmatrix_reset_hold(Object *obj, ResetType type)
memset(s->irq_map, 0, sizeof(s->irq_map));
memset(s->irq_prio, 0, sizeof(s->irq_prio));
s->irq_thres = 0;
s->irq_pending = 0;
s->irq_levels = 0;
s->irq_trigger = 0;
s->irq_enabled = 0;
@@ -349,15 +242,7 @@ static void esp32c3_intmatrix_reset_hold(Object *obj, ResetType type)
static void esp32c3_intmatrix_realize(DeviceState *dev, Error **errp)
{
ESP32C3IntMatrixState *s = ESP32C3_INTMATRIX(dev);
EspRISCVCPU *cpu = s->cpu;
EspRISCVCPUClass *cpu_klass = ESP_CPU_GET_CLASS(cpu);
esp32c3_intmatrix_reset_hold(OBJECT(dev), RESET_TYPE_COLD);
/* Register MIE callback */
assert(cpu);
cpu_klass->esp_cpu_register_mie_callback(cpu, esp32c3_intmatrix_mie_enabled, s);
}
-2
View File
@@ -75,8 +75,6 @@ typedef struct ESP32C3IntMatrixState {
uint8_t irq_prio[ESP32C3_CPU_INT_COUNT + 1];
/* Current priority threshold of the CPU interrupts */
uint8_t irq_thres;
/* Keep a bitmap of the pending interrupts */
uint64_t irq_pending;
/* Bitmap that records the enabled/disabled interrupts */
uint64_t irq_enabled;
/* Bitmap that records the type of trigger for interrupts */
+35 -53
View File
@@ -123,13 +123,6 @@ static RISCVException esp_cpu_csr_write(CPURISCVState *env, int csrno, target_ul
}
static void esp_cpu_register_mie_callback(EspRISCVCPU *env, EspIntEnableCallback callback, void* opaque)
{
assert(env != NULL);
env->mie_enabled_callback = callback;
env->mie_enabled_opaque = opaque;
}
riscv_csr_operations esp_cpu_csr_ops = {
.predicate = esp_cpu_csr_predicate,
.read = esp_cpu_csr_read,
@@ -137,19 +130,15 @@ riscv_csr_operations esp_cpu_csr_ops = {
};
/**
* Checks whether the CPU can accepts interrupts or not
*/
bool esp_cpu_accept_interrupts(EspRISCVCPU *cpu)
static void esp_cpu_update_parent_irq(EspRISCVCPU *cpu)
{
/* Get the MIE bit out of the MSTATUS register */
CPURISCVState *env = &cpu->parent_obj.env;
const bool mie = (riscv_csr_read(env, CSR_MSTATUS) & MSTATUS_MIE) != 0;
return !cpu->irq_pending && mie;
if (cpu->irq_lines != 0) {
qemu_irq_raise(cpu->parent_irq);
} else {
qemu_irq_lower(cpu->parent_irq);
}
}
/**
* Function called when an interrupt is incoming.
*/
@@ -157,15 +146,34 @@ static void esp_cpu_irq_handler(void *opaque, int n, int level)
{
EspRISCVCPU *cpu = (EspRISCVCPU*) opaque;
/* Interrupt incoming if level is not 0, make sure we can receive interrupts */
if (level && esp_cpu_accept_interrupts(cpu)) {
cpu->irq_pending = true;
cpu->irq_cause = n;
qemu_irq_raise(cpu->parent_irq);
/* Lines go from 1 to 31 included */
assert(n <= ESP_CPU_INT_LINES);
if (n == 0) {
return;
}
if (level != 0) {
SET_BIT(cpu->irq_lines, n);
} else {
CLEAR_BIT(cpu->irq_lines, n);
}
esp_cpu_update_parent_irq(cpu);
}
static uint32_t esp_cpu_select_irq_cause(EspRISCVCPU *cpu)
{
for (uint32_t i = 1; i <= ESP_CPU_INT_LINES; i++) {
if (BIT_SET(cpu->irq_lines, i)) {
return i;
}
}
return 0;
}
/**
* TCG operation called when the CPU has to actually jump to the interrupt handler.
*/
@@ -176,30 +184,10 @@ static bool esp_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
* replace the most important part for us: the mcause. */
EspRISCVCPU *cpu = ESP_CPU(cs);
EspRISCVCPUClass *klass = ESP_CPU_GET_CLASS(cpu);
const uint32_t cause = esp_cpu_select_irq_cause(cpu);
if (!cpu->irq_pending) {
/* We arrive here after servicing an interrupt but haven't de-asserted the parent IRQ.
* If the CPU can now accept interrupts, invoke the callback that will check for the next
* interrupt. */
if (esp_cpu_accept_interrupts(cpu)) {
/* Mark whether we have interrupts pending or not */
bool pending = false;
if (cpu->mie_enabled_callback) {
/* If the callback schedules a new interrupt, `cpu->irq_pending` will be set after */
pending = cpu->mie_enabled_callback(cpu->mie_enabled_opaque);
}
/* If no further interrupt was scheduled OR no further interrupts are pending, lower the parent's IRQ */
if (!pending && !cpu->irq_pending) {
qemu_irq_lower(cpu->parent_irq);
}
}
/* If the CPU still doesn't accept interrupts or the callback invoked didn't schedule a new interrupt,
* return false to mark the absence of interrupt. */
if (!cpu->irq_pending) {
return false;
}
if (cause == 0) {
return false;
}
const bool accepted = klass->parent_exec_interrupt(cs, interrupt_request);
@@ -207,10 +195,6 @@ static bool esp_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
if (accepted) {
CPURISCVState *env = &cpu->parent_obj.env;
const bool vectored = (env->mtvec & 3) == 1;
const uint32_t cause = cpu->irq_cause;
/* IRQ has been acknowledged by the parent CPU, it is not pending anymore */
cpu->irq_pending = false;
/* Update the mcause and the relevant PC */
env->mcause = RISCV_EXCP_INT_FLAG | cause;
@@ -219,6 +203,7 @@ static bool esp_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
env->pc = (env->mtvec >> 2 << 2) + (vectored ? cause * 4 : 0);
}
/* Similarly, make sure the parent IRQ reflects the current state */
return accepted;
}
@@ -234,7 +219,7 @@ static void set_misa(CPURISCVState *env, RISCVMXL mxl, uint32_t ext)
static void esp_cpu_reset(void *opaque)
{
EspRISCVCPU *cpu = opaque;
cpu->irq_pending = 0;
cpu->irq_lines = 0;
qemu_irq_lower(cpu->parent_irq);
cpu_reset(CPU(cpu));
}
@@ -343,9 +328,6 @@ static void esp_cpu_class_init(ObjectClass *klass, void *data)
/* Save the parent realize function in order to be able to call it later */
device_class_set_parent_realize(dc, esp_cpu_realize,
&cpuclass->parent_realize);
/* Function to register MIE callback */
cpuclass->esp_cpu_register_mie_callback = esp_cpu_register_mie_callback;
}
static const TypeInfo esp_cpu_info = {
+2 -29
View File
@@ -41,15 +41,6 @@ typedef struct {
uint64_t divider;
} ESPCPUCycleCounter;
/**
* @brief Callback type called when MIE status bit is re-enabled
*
* @param Opaque context given when registering the callback
*
* @returns true if any interrupt is pending, false is no interrupt is pending
*/
typedef bool (*EspIntEnableCallback)(void*);
/**
* Espressif's RISC-V core is different from standard RISC-V because of the way interrupts are handled.
* Extend the standard RISC-V core implementation.
@@ -62,21 +53,13 @@ typedef struct EspRISCVCPU {
ESPCPUCycleCounter cc_user;
ESPCPUCycleCounter cc_machine;
/* Callback called when the interrupts are re-enabled */
EspIntEnableCallback mie_enabled_callback;
void* mie_enabled_opaque;
/*< public >*/
/* The parent object already has a reset vector property */
uint32_t hartid_base;
/* Parent IRQ_M line */
qemu_irq parent_irq;
/* Number of the IRQ that triggered the interrupt */
uint32_t irq_cause;
/* Interrupts are not always synchronous, so MIE may still be set to 1 while an
* interrupt is waiting to be handled. So, keep a mirrored MIE to mark whether
* we can receive interrupts or not. */
bool irq_pending;
/* Bitmap of interrupt matrix output lines currently asserted on CPU inputs. */
uint32_t irq_lines;
} EspRISCVCPU;
@@ -87,14 +70,4 @@ typedef struct EspRISCVCPUClass {
DeviceReset parent_reset;
bool (*parent_exec_interrupt)(CPUState *cpu, int interrupt_request);
/*< public >*/
void (*esp_cpu_register_mie_callback)(EspRISCVCPU *env, EspIntEnableCallback callback, void* opaque);
} EspRISCVCPUClass;
/**
* @brief Check whether the current CPU state can accept interrupts or not.
* If an interrupt is currently pending and the MEPC was not set to the reset vector yet,
* this function returns false.
* If MIE bit is not set in the MSTATUS register, it will also return false.
*/
bool esp_cpu_accept_interrupts(EspRISCVCPU *cpu);