Page 1 of 1

COM1 IRQ Not Triggering Every Time

Posted: Sun Mar 24, 2024 2:31 pm
by mttarry
When I send serial data via QEMU's virtual serial port from my host PC to my guest OS, I am seeing that my IRQ4 for COM1 does not trigger every time. On average it will trigger about 50% of the time. I can't figure out how I can determine whether it is QEMU errata or an issue with how I've setup my PIC. The fact that it triggers some of the time at least tells me that I have my interrupts mapped correctly, and that the IRQ handler is in the right place.

I'm looking for any advice that might help me determine why IRQ4 is not triggering for each byte of serial data that is sent. I have not tested this on hardware yet because I don't have a serial cable at the moment. Thank you in advance for any help you can provide.

These are my QEMU flags:

Code: Select all

QEMU_FLAGS = -cdrom $(ISO_TARGET) -display cocoa,full-screen=on -monitor stdio -serial pty
And I'm sending data from my host via:

Code: Select all

echo -n h > /dev/ttys002
About 50% of the time I issue this command from my host, an IRQ is not triggered on my kernel.

Here is the relevant code:

irq.c

Code: Select all

void irq_remap(void) {
    // ICW1
    outb(PIC1_CMD, ICW1_ICW4 | ICW1_INIT);
    outb(PIC2_CMD, ICW1_ICW4 | ICW1_INIT);

    // ICW2
    outb(PIC1_DATA, ICW2_PIC1_BASE);
    outb(PIC2_DATA, ICW2_PIC2_BASE);

    // ICW3
    outb(PIC1_DATA, ICW3_PIC1_IRQ); // master PIC at IRQ4
    outb(PIC2_DATA, ICW3_PIC2_IRQ); // slave PIC at IRQ2

    // ICW4
    outb(PIC1_DATA, ICW4_8086);
    outb(PIC2_DATA, ICW4_8086);

    // Unmask IRQ4 -- Todo: handle this in separate logic
    outb(PIC1_DATA, 0xEF);   
	outb(PIC2_DATA, 0xFF);
}

static uint16_t pic_get_irq_reg(int ocw3) {
    outb(PIC1_CMD, ocw3);
    outb(PIC2_CMD, ocw3);
    return (inb(PIC2_CMD) << 8) | inb(PIC1_CMD);
}
 
uint16_t pic_get_irr(void) {
    return pic_get_irq_reg(PIC_OCW3_IRR);
}

uint16_t pic_get_isr(void) {
    return pic_get_irq_reg(PIC_OCW3_ISR);
}
 
void pic_endof_int(uint8_t irq) {
	if (irq >= 8) 
		outb(PIC2_CMD, PIC_EOI);
 
	outb(PIC1_CMD, PIC_EOI);
}

void irq_handler() {
    int irq = BIT_INDEX(pic_get_isr());
    kprintf(".");

    if (irq_handlers[irq] != NULL) {
        irq_handlers[irq]();
    }

    outb(PIC1_DATA, 0xEF);   
	outb(PIC2_DATA, 0xFF);

    pic_endof_int(irq);
}


void irq_set_gates() {
    for (int i = IRQ_VEC_START; i < IRQ_VEC_END; ++i) {
        idt_set_desc(i, &irq_stub, IDTENTRY_KERNEL_INT);
    }
}

void irq_install() {
    irq_remap();
    irq_set_gates();
}

void register_irq_handler(int irq_num, void* handler) {
    if (irq_num >= 0 && irq_num < MAX_IRQS) {
        irq_handlers[irq_num] = handler;
    }
}
serial.c

Code: Select all

int serial_init(void) {
    outb(SERIAL_PORT + IER_OFFSET, 0x01); // enable interrupts

    outb(SERIAL_PORT + LCR_OFFSET, 0x80); // set DLAB
    outb(SERIAL_PORT + DIVISOR_LSB_OFFSET, 0x01); // set LSB of divisor
    outb(SERIAL_PORT + DIVISOR_MSB_OFFSET, 0x00); // set MSB of divisor
    outb(SERIAL_PORT + LCR_OFFSET, 0x00); // clear DLAB

    //outb(SERIAL_PORT + FCR_OFFSET, 0xC7); // enable FIFO, clear buffers, 14-byte threshold
    outb(SERIAL_PORT + FCR_OFFSET, 0x00); // disable FIFO

    outb(SERIAL_PORT + LCR_OFFSET, 0x03); // 8-bit, no parity, 1 stop bit
    outb(SERIAL_PORT + MCR_OFFSET, 0x1E); // loopback mode, test the serial chip
    outb(SERIAL_PORT, 0xAB);

    if (inb(SERIAL_PORT) != 0xAB) {
        return 1;
    }

    // serial is not faulty, set in normal operation mode
    outb(SERIAL_PORT + MCR_OFFSET, 0x0F); // interrupt enabled, RTS and DSR set
    
    register_irq_handler(SERIAL_PORT_IRQ, serial_irq_handler);

    return 0;
}

static void serial_handle_receive_byte() {
    uint8_t recb = inb(SERIAL_PORT);
    kprintf("%c", recb);
}

static void serial_irq_handler() {
    // Determine if we are reading/writing UART
    uint8_t lsr = inb(SERIAL_PORT + LSR_OFFSET);
    uint8_t iir = inb(SERIAL_PORT + IIR_OFFSET);

    // Check if data is ready to be extracted from the UART
    if (lsr & 1) {
        serial_handle_receive_byte();
    }
}

}

Re: COM1 IRQ Not Triggering Every Time

Posted: Sun Mar 24, 2024 9:49 pm
by Octocontrabass
mttarry wrote:

Code: Select all

    outb(SERIAL_PORT + DIVISOR_LSB_OFFSET, 0x01); // set LSB of divisor
    outb(SERIAL_PORT + DIVISOR_MSB_OFFSET, 0x00); // set MSB of divisor
Have you tried a bigger divisor? Maybe it's just too fast without the FIFO.

Re: COM1 IRQ Not Triggering Every Time

Posted: Mon Mar 25, 2024 4:10 pm
by mttarry
Octocontrabass wrote:
mttarry wrote:

Code: Select all

    outb(SERIAL_PORT + DIVISOR_LSB_OFFSET, 0x01); // set LSB of divisor
    outb(SERIAL_PORT + DIVISOR_MSB_OFFSET, 0x00); // set MSB of divisor
Have you tried a bigger divisor? Maybe it's just too fast without the FIFO.
I just tried increasing the divisor ( tried values 0x01-0xFF), and still see the same behavior. I also repeated that with the FIFO being enabled. Out of 20 runs of

Code: Select all

echo -n "hi" > /dev/ttys002
, the IRQ4 will trigger anywhere between 8-14 times. At this point I'm thinking that maybe it's something to do with the MacOS port of qemu-system-i386, but any other ideas would be greatly appreciated.

Re: COM1 IRQ Not Triggering Every Time

Posted: Mon Mar 25, 2024 6:24 pm
by Octocontrabass
The only other thing that caught my eye is that you're reading the interrupt controller's in-service register to check which interrupt you're handling. Have you tried passing the interrupt vector from your interrupt stub instead?

Re: COM1 IRQ Not Triggering Every Time

Posted: Mon Mar 25, 2024 6:44 pm
by mttarry
I've just identified that with QEMU -serial stdio, my COM1 IRQ fires 100% of the time and I can retrieve the serial data that way. For some reason the -serial pty option, which opens a virtual serial device file on my host, causes the interrupt not to fire all the time.