blob: b9e093d087edbc174fb735b0a60cc6267ebd6cf2 [file]
/*
* Copyright 2019, Data61
* Commonwealth Scientific and Industrial Research Organisation (CSIRO)
* ABN 41 687 119 230.
*
* This software may be distributed and modified according to the terms of
* the GNU General Public License version 2. Note that NO WARRANTY is provided.
* See "LICENSE_GPLv2.txt" for details.
*
* @TAG(DATA61_GPL)
*/
/* Intel 8259 Programmable Interrupt Controller (PIC) emulator on x86.
*
* The functions related to machine state manipulations were taken
* from Linux kernel 3.8.8 arch/x86/kvm/i8259.c
*
*/
#include <autoconf.h>
#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <sel4/sel4.h>
#include <stdio.h>
#include <utils/util.h>
#include <sel4vm/guest_vm.h>
#include <sel4vm/boot.h>
#include <sel4vm/guest_irq_controller.h>
#include <sel4vm/arch/ioports.h>
#include "i8259.h"
#define I8259_MASTER 0
#define I8259_SLAVE 1
#define PIC_NUM_PINS 16
/*first programmable interrupt controller, master*/
#define X86_IO_PIC_1_START 0x20
#define X86_IO_PIC_1_END 0x21
/*second programmable interrupt controller, slave*/
#define X86_IO_PIC_2_START 0xa0
#define X86_IO_PIC_2_END 0xa1
/*ELCR (edge/level control register) for IRQ line*/
#define X86_IO_ELCR_START 0x4d0
#define X86_IO_ELCR_END 0x4d1
typedef struct i8259_irq_ack {
irq_ack_fn_t callback;
void *cookie;
} i8259_irq_ack_t;
static i8259_irq_ack_t irq_ack_fns[PIC_NUM_PINS];
/* PIC Machine state. */
struct i8259_state {
unsigned char last_irr; /* Edge detection */
unsigned char irr; /* Interrupt request register */
unsigned char imr; /* Interrupt mask register */
unsigned char isr; /* Interrupt service register */
unsigned char priority_add; /* Highest irq priority */
unsigned char irq_base;
unsigned char read_reg_select;
unsigned char poll;
unsigned char special_mask;
unsigned char init_state;
unsigned char auto_eoi;
unsigned char rotate_on_auto_eoi;
unsigned char special_fully_nested_mode;
unsigned char init4; /* True if 4 byte init */
unsigned char elcr; /* PIIX edge/trigger selection */
unsigned char elcr_mask;
unsigned char isr_ack; /* Interrupt ack detection */
struct i8259 *pics_state;
};
/* Struct containig PIC state for a Guest OS instance. */
struct i8259 {
unsigned int wakeup_needed;
unsigned int pending_acks;
struct i8259_state pics[2]; /* 0 is master pic, 1 is slave pic */
int output; /* Intr from master PIC */
int emitagain;
};
static inline int select_pic(unsigned int irq)
{
assert(irq < 16);
if (irq < 8) {
return I8259_MASTER;
} else {
return I8259_SLAVE;
}
}
static inline int __vm_irq_line_state(unsigned long *irq_state,
int irq_source_id, int level)
{
/* Logical OR for level trig interrupt. */
if (level) {
(*irq_state) |= BIT(irq_source_id);
} else {
(*irq_state) &= ~BIT(irq_source_id);
}
return !!(*irq_state);
}
/* Return the highest priority found in mask (highest = smallest number). Return 8 if no irq */
static inline int get_priority(struct i8259_state *s, int mask)
{
int priority = 0;
if (!mask) {
return 8;
}
while (!(mask & (1 << ((priority + s->priority_add) & 7)))) {
priority++;
}
return priority;
}
/* Check if given IO address is valid. */
static int i8259_in_range(unsigned int addr)
{
switch (addr) {
case 0x20:
case 0x21:
case 0xa0:
case 0xa1:
case 0x4d0:
case 0x4d1:
return 1;
default:
return 0;
}
}
/* Compare ISR with the highest priority IRQ in IRR.
* Returns -1 if no interrupts,
* Otherwise returns the PIC interrupt generated.
*/
static int pic_get_irq(struct i8259_state *s)
{
int mask, cur_priority, priority;
mask = s->irr & ~s->imr;
priority = get_priority(s, mask);
if (priority == 8) {
return -1;
}
/* Compute current priority. If special fully nested mode on the
* master, the IRQ coming from the slave is not taken into account
* for the priority computation.
*/
mask = s->isr;
if (s->special_fully_nested_mode && s == &s->pics_state->pics[0]) {
mask &= ~(1 << 2);
}
cur_priority = get_priority(s, mask);
if (priority < cur_priority) {
/* Higher priority found: an irq should be generated. */
return (priority + s->priority_add) & 7;
} else {
return -1;
}
}
/* Clear the IRQ from ISR, the IRQ has been served. */
static void pic_clear_isr(vm_t *vm, struct i8259_state *s, int irq)
{
/* Clear the ISR, notify the ack handler. */
s->isr &= ~(1 << irq);
if (s != &s->pics_state->pics[0]) {
irq += 8;
}
if (irq != 2) {
if (irq_ack_fns[irq].callback) {
irq_ack_fns[irq].callback(vm->vcpus[BOOT_VCPU], irq, irq_ack_fns[irq].cookie);
}
}
}
/* Set irq level. If an edge is detected, then the IRR is set to 1. */
static inline int pic_set_irq1(struct i8259_state *s, int irq, int level)
{
int mask, ret = 1;
mask = 1 << irq;
if (s->elcr & mask) {
/* Level triggered. */
if (level) {
ret = !(s->irr & mask);
s->irr |= mask;
s->last_irr |= mask;
} else {
s->irr &= ~mask;
s->last_irr &= ~mask;
}
} else {
/* Edge triggered. */
if (level) {
if ((s->last_irr & mask) == 0) {
ret = !(s->irr & mask);
s->irr |= mask;
}
s->last_irr |= mask;
} else {
s->last_irr &= ~mask;
}
}
return (s->imr & mask) ? -1 : ret;
}
/* Raise IRQ on CPU if necessary. Must be called every time the active IRQ may change.
Update the master pic and trigger interrupt injection according to the IRR and ISR. */
static void pic_update_irq(struct i8259 *s)
{
int irq2, irq;
irq2 = pic_get_irq(&s->pics[1]);
if (irq2 >= 0) {
/* If IRQ request by slave PIC, signal master PIC and set the IRR in master PIC. */
pic_set_irq1(&s->pics[0], 2, 1);
pic_set_irq1(&s->pics[0], 2, 0);
}
irq = pic_get_irq(&s->pics[0]);
/* PIC status changed injection flag. */
if (!s->output) {
s->wakeup_needed = true;
}
if (irq >= 0) {
s->output = 1;
} else {
s->output = 0;
}
if (s->emitagain && s->output) {
// haveint_emit();
s->emitagain = 0;
}
}
/* Reset the PIC state for a guest OS. */
static void pic_reset(vm_t *vm, struct i8259_state *s)
{
int irq;
unsigned char edge_irr = s->irr & ~s->elcr;
s->last_irr = 0;
s->irr &= s->elcr;
s->imr = 0;
s->priority_add = 0;
s->special_mask = 0;
s->read_reg_select = 0;
if (!s->init4) {
s->special_fully_nested_mode = 0;
s->auto_eoi = 0;
}
s->init_state = 1;
#if 0
/* FIXME: CONNECT pic with APIC */
kvm_for_each_vcpu(i, vcpu, s->piics_state->kvm)
if (kvm_apic_accept_pic_intr(vcpu)) {
found = true;
break;
}
if (!found) {
return;
}
#endif
for (irq = 0; irq < PIC_NUM_PINS / 2; irq++) {
if (edge_irr & (1 << irq)) {
pic_clear_isr(vm->vcpus[BOOT_VCPU], s, irq);
}
}
}
/* Write into the state owned by the guest OS. */
static void pic_ioport_write(vm_vcpu_t *vcpu, struct i8259_state *s, unsigned int addr, unsigned int val)
{
int priority, cmd, irq;
addr &= 1;
if (addr == 0) {
if (val & 0x10) {
/* ICW1 */
s->init4 = val & 1;
if (val & 0x02) {
printf("PIC: single mode not supported\n");
}
if (val & 0x08) {
printf("PIC: level sensitive irq not supported\n");
}
/* Reset the machine state and pending IRQS. */
pic_reset(vcpu, s);
} else if (val & 0x08) {
/* OCW 3 */
if (val & 0x04) {
s->poll = 1;
}
if (val & 0x02) {
s->read_reg_select = val & 1;
}
if (val & 0x40) {
s->special_mask = (val >> 5) & 1;
}
} else {
/* OCW 2 */
cmd = val >> 5;
switch (cmd) {
case 0:
case 4:
s->rotate_on_auto_eoi = cmd >> 2;
break;
case 1:
/* End of interrupt. */
case 5:
/* Clear ISR and update IRQ*/
priority = get_priority(s, s->isr);
if (priority != 8) {
irq = (priority + s->priority_add) & 7;
if (cmd == 5) {
s->priority_add = (irq + 1) & 7;
}
pic_clear_isr(vcpu->vm, s, irq);
pic_update_irq(s->pics_state);
}
break;
case 3:
/* Specific EOI command. */
irq = val & 7;
pic_clear_isr(vcpu->vm, s, irq);
pic_update_irq(s->pics_state);
break;
case 6:
/* Set priority command. */
s->priority_add = (val + 1) & 7;
pic_update_irq(s->pics_state);
break;
case 7:
/* Rotate on specific eoi command. */
irq = val & 7;
s->priority_add = (irq + 1) & 7;
pic_clear_isr(vcpu->vm, s, irq);
pic_update_irq(s->pics_state);
break;
default:
/* No operation. */
break;
}
}
} else
switch (s->init_state) {
case 0: { /* Normal mode OCW 1. */
unsigned char imr_diff = s->imr ^ val;
(void) imr_diff;
//off = (s == &s->pics_state->pics[0]) ? 0 : 8;
s->imr = val;
#if 0
for (irq = 0; irq < PIC_NUM_PINS / 2; irq++)
if (imr_diff & (1 << irq))
/*FIXME: notify the status changes for IMR*/
kvm_fire_mask_notifiers(
s->pics_state->kvm,
select_pic(irq + off),
irq + off,
!!(s->imr & (1 << irq)));
#endif
pic_update_irq(s->pics_state);
break;
}
case 1:
/* ICW 2 */
s->irq_base = val & 0xf8;
s->init_state = 2;
break;
case 2:
if (s->init4) {
s->init_state = 3;
} else {
s->init_state = 0;
}
break;
case 3:
/* ICW 4 */
s->special_fully_nested_mode = (val >> 4) & 1;
s->auto_eoi = (val >> 1) & 1;
s->init_state = 0;
break;
}
}
/* Poll the pending IRQS for the highest priority IRQ, ack the IRQ: clear the ISR and IRR, and
* update PIC state. Returns -1 if no pending IRQ. */
static unsigned int pic_poll_read(vm_t *vm, struct i8259_state *s, unsigned int addr1)
{
unsigned int ret;
ret = pic_get_irq(s);
if (ret >= 0) {
if (addr1 >> 7) {
s->pics_state->pics[0].isr &= ~(1 << 2);
s->pics_state->pics[0].irr &= ~(1 << 2);
}
s->irr &= ~(1 << ret);
pic_clear_isr(vm, s, ret);
if (addr1 >> 7 || ret != 2) {
pic_update_irq(s->pics_state);
}
} else {
ret = 0x07;
pic_update_irq(s->pics_state);
}
return ret;
}
/* Read and write functions for PIC (master and slave). */
static unsigned int pic_ioport_read(vm_vcpu_t *vcpu, struct i8259_state *s, unsigned int addr)
{
unsigned int ret;
/* Poll for the highest priority IRQ. */
if (s->poll) {
ret = pic_poll_read(vcpu, s, addr);
s->poll = 0;
} else {
if (!(addr & 1)) {
if (s->read_reg_select) {
ret = s->isr;
} else {
ret = s->irr;
}
} else {
ret = s->imr;
}
}
return ret;
}
/*read and write functions for ELCR (edge/level control registers)
IO: 0x4d0 0x4d1 each bit corresponsing to an IRQ from 8259
bit set: level triggered mode
bit clear: edge triggered mode*/
static void elcr_ioport_write(struct i8259_state *s, unsigned int addr, unsigned int val)
{
s->elcr = val & s->elcr_mask;
}
static unsigned int elcr_ioport_read(struct i8259_state *s, unsigned int addr)
{
return s->elcr;
}
ioport_fault_result_t i8259_port_out(vm_vcpu_t *vcpu, void *cookie, unsigned int port_no, unsigned int size,
unsigned int value)
{
/* Sender thread is the VMM main thread, calculate guest ID according to the badge. */
struct i8259 *s = vcpu->vm->arch.i8259_gs;
if (!i8259_in_range(port_no)) {
return IO_FAULT_ERROR;
}
if (size != 1) {
return IO_FAULT_ERROR;
}
/* 0x20, 0x21, master pic, 0xa0, 0xa1 slave PIC. */
switch (port_no) {
case 0x20:
case 0x21:
case 0xa0:
case 0xa1:
pic_ioport_write(vcpu, &s->pics[port_no >> 7], port_no, value);
break;
case 0x4d0:
case 0x4d1:
elcr_ioport_write(&s->pics[port_no & 1], port_no, value);
break;
}
return IO_FAULT_HANDLED;
}
ioport_fault_result_t i8259_port_in(vm_vcpu_t *vcpu, void *cookie, unsigned int port_no, unsigned int size,
unsigned int *result)
{
/* Sender thread is the VMM main thread, calculate guest ID according to the badge. */
struct i8259 *s = vcpu->vm->arch.i8259_gs;
if (!i8259_in_range(port_no)) {
return IO_FAULT_ERROR;
}
if (size != 1) {
return IO_FAULT_ERROR;
}
/* 0x20, 0x21, master pic, 0xa0, 0xa1 slave PIC. */
switch (port_no) {
case 0x20:
case 0x21:
case 0xa0:
case 0xa1:
*result = pic_ioport_read(vcpu, &s->pics[port_no >> 7], port_no);
break;
case 0x4d0:
case 0x4d1:
*result = elcr_ioport_read(&s->pics[port_no & 1], port_no);
break;
}
return IO_FAULT_HANDLED;
}
/* Init internal status for PIC driver. */
static void i8259_init_state(struct i8259 *s)
{
/* Init pic machine state for guest OS. */
// s->pics[0].elcr = seL4_IA32_IOPort_In8(LIB_VMM_IO_PCI_CAP, 0x4d0).result;
// s->pics[1].elcr = seL4_IA32_IOPort_In8(LIB_VMM_IO_PCI_CAP, 0x4d1).result;
s->pics[0].elcr = 0;
s->pics[1].elcr = 0;
s->pics[0].elcr_mask = 0xf8;
s->pics[1].elcr_mask = 0xde;
s->pics[0].pics_state = s;
s->pics[1].pics_state = s;
}
/* Acknowledge interrupt IRQ. */
static inline void pic_intack(vm_t *vm, struct i8259_state *s, int irq)
{
/* Ack the IRQ, set the ISR. */
s->isr |= 1 << irq;
/* We don't clear a level sensitive interrupt here. */
if (!(s->elcr & (1 << irq))) {
s->irr &= ~(1 << irq);
}
/* Clear the ISR for auto EOI mode. */
if (s->auto_eoi) {
if (s->rotate_on_auto_eoi) {
s->priority_add = (irq + 1) & 7;
}
pic_clear_isr(vm, s, irq);
}
}
/* Use output as a flag for pending IRQ. */
static int i8259_has_irq(vm_t *vm)
{
struct i8259 *s = vm->arch.i8259_gs;
return s->output;
}
#if 0
static int i8259_poll_irq()
{
struct i8259 *s = &i8259_gs;
int irq, irq2, intno;
/* Search for the highest priority IRQ. */
irq = pic_get_irq(&s->pics[0]);
if (irq >= 0) {
if (irq == 2) {
irq2 = pic_get_irq(&s->pics[1]);
if (irq2 >= 0) {
} else {
/* Spurious IRQ on slave controller. */
irq2 = 7;
}
intno = s->pics[1].irq_base + irq2;
irq = irq2 + 8;
} else {
intno = s->pics[0].irq_base + irq;
}
} else {
/* Spurious IRQ on host 8259 controller. */
irq = 7;
intno = s->pics[0].irq_base + irq;
}
return intno;
}
#endif
/* Return the highest pending IRQ. Ack the IRQ by updating the ISR before entering guest, using this
* function to get the pending IRQ. */
static int i8259_read_irq(vm_t *vm)
{
struct i8259 *s = vm->arch.i8259_gs;
int irq, irq2, intno;
/* Search for the highest priority IRQ. */
irq = pic_get_irq(&s->pics[0]);
if (irq >= 0) {
/* Ack the IRQ. */
pic_intack(vm, &s->pics[0], irq);
/* Ack the slave 8259 controller. */
if (irq == 2) {
irq2 = pic_get_irq(&s->pics[1]);
if (irq2 >= 0) {
pic_intack(vm, &s->pics[1], irq2);
} else
/* Spurious IRQ on slave controller. */
{
irq2 = 7;
}
intno = s->pics[1].irq_base + irq2;
irq = irq2 + 8;
} else {
intno = s->pics[0].irq_base + irq;
}
} else {
/* Spurious IRQ on host 8259 controller. */
irq = 7;
intno = s->pics[0].irq_base + irq;
}
pic_update_irq(s);
return intno;
}
int i8259_get_interrupt(vm_t *vm)
{
int ret;
if (i8259_has_irq(vm)) {
ret = i8259_read_irq(vm);
} else {
ret = -1;
}
if (!i8259_has_irq(vm)) {
vm->arch.i8259_gs->emitagain = 1;
}
return ret;
}
int i8259_has_interrupt(vm_t *vm)
{
int ret = i8259_has_irq(vm);
return ret;
}
vm_ioport_entry_t pic_ioports[] = {
{{X86_IO_PIC_1_START, X86_IO_PIC_1_END}, {NULL, i8259_port_in, i8259_port_out, "8259 Programmable Interrupt Controller (1st, Master)"}},
{{X86_IO_PIC_2_START, X86_IO_PIC_2_END}, {NULL, i8259_port_in, i8259_port_out, "8259 Programmable Interrupt Controller (2nd, Slave)"}},
{{X86_IO_ELCR_START, X86_IO_ELCR_END}, {NULL, i8259_port_in, i8259_port_out, "ELCR (edge/level control register) for IRQ line"}}
};
int i8259_pre_init(vm_t *vm)
{
int err;
/* First initialize the emulated pic state */
vm->arch.i8259_gs = calloc(1, sizeof(struct i8259));
if (!vm->arch.i8259_gs) {
return -1;
}
i8259_init_state(vm->arch.i8259_gs);
vm->arch.i8259_gs->emitagain = 1;
for (int i = 0; i < ARRAY_SIZE(pic_ioports); i++) {
vm_ioport_range_t pic_range = pic_ioports[i].range;
vm_ioport_interface_t pic_interface = pic_ioports[i].interface;
err = vm_io_port_add_handler(vm, pic_range, pic_interface);
if (err) {
return err;
}
}
return 0;
}
/* This is the actual function that will get called for all interrupt events
* Furthermore this implements the guest irq controller interface */
/* To inject an IRQ: First set the level as 1, then set the level as 0, toggling the level for
* triggering the IRQ.
* IRQ source ID is used for mapping multiple IRQ source into a IRQ pin.
* Sets irq request into the state machine for PIC.
*/
int vm_set_irq_level(vm_vcpu_t *vcpu, int irq, int irq_level)
{
int ret;
struct i8259 *s = vcpu->vm->arch.i8259_gs;
/* Set IRR. */
ret = pic_set_irq1(&s->pics[irq >> 3], irq & 7, irq_level);
pic_update_irq(s);
if (ret == -1) {
return -1;
}
return 0;
}
int vm_inject_irq(vm_vcpu_t *vcpu, int irq)
{
vm_set_irq_level(vcpu, irq, 1);
vm_set_irq_level(vcpu, irq, 0);
return 0;
}
int vm_register_irq(vm_vcpu_t *vcpu, int irq, irq_ack_fn_t fn, void *cookie)
{
if (irq < 0 || irq >= PIC_NUM_PINS) {
ZF_LOGE("irq %d is invalid", irq);
return -1;
}
i8259_irq_ack_t *ack = &irq_ack_fns[irq];
ack->callback = fn;
ack->cookie = cookie;
return 0;
}