I'm developing a toy unix clone and I'm trying to wire up my interrupts properly. I've run into a problem where my Keyboard IRQ (IRQ 1) fires just once even after I properly acknowledge it and so on. I've enabled the PIT interrupt as well, to double check if my ACKs are going to the PIC okay, and that seems to work fine. (fires multiple times)
One catch with interrup.s is that I'm passing the struct register_t on the stack (by value) and the compiler was trashing that after it returns from the C interrupt handler. Surprisingly, the only value that was getting trashed was the value on the top of the stack (The data segment register in this case) and I've verified that the rest of the values in the stack look okay by printing the stack before and after the call to the interrupt handler occurs. I've added a temporary work-around to fix this issue, but I will clean this up later.
I've also verified that software interrupts work fine by triggering int $3 multiple times.
Any advice is appreciated! Here's the code:
interrupt.s
.macro ISR_NOERRCODE int_no # A macro for ISRs that don't push an error code
.global isr\int_no
isr\int_no:
cli
push $0
push $\int_no
jmp isr_irq_common_stub
.endm
.macro ISR_ERRORCODE int_no # A macro for ISRs that do push an error code
.global isr\int_no
isr\int_no:
cli
push $\int_no
jmp isr_irq_common_stub
.endm
.macro IRQ irq_no, isr_map # A macro for IRQs from the PIC
.global irq\irq_no
irq\irq_no:
xchgw %bx, %bx
cli
push $0 # Error code
push $\isr_map # Interrupt number
jmp isr_irq_common_stub
.endm
ISR_NOERRCODE 0
ISR_NOERRCODE 1
ISR_NOERRCODE 2
ISR_NOERRCODE 3
ISR_NOERRCODE 4
ISR_NOERRCODE 5
ISR_NOERRCODE 6
ISR_NOERRCODE 7
ISR_ERRORCODE 8 # ISR 8 pushes error code onto stack
ISR_NOERRCODE 9
ISR_ERRORCODE 10 # ISR 10 - 14 push error codes onto stack
ISR_ERRORCODE 11
ISR_ERRORCODE 12
ISR_ERRORCODE 13
ISR_ERRORCODE 14
ISR_NOERRCODE 15
ISR_NOERRCODE 16
ISR_ERRORCODE 17
ISR_NOERRCODE 18
ISR_NOERRCODE 19
ISR_NOERRCODE 20
ISR_NOERRCODE 21
ISR_NOERRCODE 22
ISR_NOERRCODE 23
ISR_NOERRCODE 24
ISR_NOERRCODE 25
ISR_NOERRCODE 26
ISR_NOERRCODE 27
ISR_NOERRCODE 28
ISR_NOERRCODE 29
ISR_ERRORCODE 30
ISR_NOERRCODE 31
IRQ 0, 32
IRQ 1, 33
IRQ 2, 34
IRQ 3, 35
IRQ 4, 36
IRQ 5, 37
IRQ 6, 38
IRQ 7, 39
IRQ 8, 40
IRQ 9, 41
IRQ 10, 42
IRQ 11, 43
IRQ 12, 44
IRQ 13, 45
IRQ 14, 46
IRQ 15, 47
# This is in isr.c
.extern isr_irq_handler
# This is our common isr stub. It saves the processor state, sets up for kernel
# mode segments, calls the C-level fault handler, and finally restores the stack
# frame
isr_irq_common_stub:
pusha # Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax
mov %ds, %ax # Lower 16-bits of eax = ds.
push %eax # save the data segment descriptor
mov $0x10, %ax # load the kernel data segment descriptor
mov %ax, %ds # Right now, we dont really have to do this
mov %ax, %es # but after we enter the user mode, the segment
mov %ax, %fs # registers will be different (0x18? and 0x20?)
mov %ax, %gs
call isr_irq_handler
# This does not work because the structure value we passed earlier
# is being messed up by the compiler. It does not preserve the previous eax
# we pushed on to the stack.
pop %eax
mov $0x10, %ax # reload the original data segment descriptor
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
popa # Pops edi,esi,ebp...
add $8, %esp # Cleans up the pushed error code and pushed ISR number
sti
iret # pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP
isr.h
#ifndef __isr_h
#define __isr_h
#include <stdint.h>
struct Registers {
uint32_t ds;
uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
uint32_t int_no, err_code;
uint32_t eip, cs, eflags, useresp, ss;
} __attribute__((packed));
typedef struct Registers register_t;
typedef void (*isr_t)(registers_t);
void register_interrupt_handler(uint8_t n, isr_t handler);
void isr_irq_handler(register_t regs);
#endif
isr.c
#include <kernel/isr.h>
#include <kernel/pic.h>
#include <stdio.h>
#include <log.h>
#include <kernel/tty.h>
isr_t interrupt_handlers[256];
void register_interrupt_handler(uint8_t n, isr_t handler)
{
interrupt_handlers[n] = handler;
}
void isr_irq_handler(register_t regs)
{
printf("Received ISR/IRQ: %d\n", regs.int_no);
if (interrupt_handlers[regs.int_no]) {
interrupt_handlers[regs.int_no]();
}
if (regs.int_no >= 32 && regs.int_no <= 47) {
pic_send_eoi(regs.int_no - 32);
}
return;
}
pic.c
#include <kernel/pic.h>
#include <asm.h>
#define PIC1 0x20 /* IO base address for master PIC */
#define PIC2 0xA0 /* IO base address for slave PIC */
#define PIC1_COMMAND PIC1
#define PIC1_DATA (PIC1+1)
#define PIC2_COMMAND PIC2
#define PIC2_DATA (PIC2+1)
#define PIC_EOI 0x20 /* End-of-interrupt command code */
#define ICW1_ICW4 0x01 /* ICW4 (not) needed */
#define ICW1_SINGLE 0x02 /* Single (cascade) mode */
#define ICW1_INTERVAL4 0x04 /* Call address interval 4 (8) */
#define ICW1_LEVEL 0x08 /* Level triggered (edge) mode */
#define ICW1_INIT 0x10 /* Initialization - required! */
#define ICW4_8086 0x01 /* 8086/88 (MCS-80/85) mode */
#define ICW4_AUTO 0x02 /* Auto (normal) EOI */
#define ICW4_BUF_SLAVE 0x08 /* Buffered mode/slave */
#define ICW4_BUF_MASTER 0x0C /* Buffered mode/master */
#define ICW4_SFNM 0x10 /* Special fully nested (not) */
#define PIC_READ_IRR 0x0a /* OCW3 irq ready next CMD read */
#define PIC_READ_ISR 0x0b /* OCW3 irq service next CMD read */
#define PIC1_OFFSET 0x20
#define PIC2_OFFSET 0x28
static void pic_mask(int pic_num, uint16_t mask);
static uint16_t pic_get_irq_reg(int ocw3);
static uint16_t pic_get_irr(void);
static uint16_t pic_get_isr(void);
void setup_remap_pics()
{
uint8_t a1, a2;
// Save existing masks
a1 = inb(PIC1_DATA);
a2 = inb(PIC2_DATA);
outb(PIC1_COMMAND, ICW1_INIT);
io_wait();
outb(PIC2_COMMAND, ICW1_INIT);
io_wait();
outb(PIC1_DATA, PIC1_OFFSET); // We're remapping the PICs interrupt codes from (0x07-0x7F) to (offset, offset + 8)
io_wait();
outb(PIC2_DATA, PIC2_OFFSET);
io_wait();
outb(PIC1_DATA, 4); // Tell the master PIC that there is a slave PIC at IRQ2 (00000100)
io_wait();
outb(PIC2_DATA, 2); // Tell the slave pic it's cascade identity (00000010)
io_wait();
outb(PIC1_DATA, ICW4_8086);
io_wait();
outb(PIC2_DATA, ICW4_8086);
io_wait();
// Restore saved masks
outb(PIC1_DATA, a1);
outb(PIC2_DATA, a2);
enable_interrupts();
// Mask everything except the keyboard, timer
pic_mask(1, 0xFD);
pic_mask(2, 0xFF);
}
static void pic_mask(int pic_num, uint16_t mask) {
uint16_t port = (pic_num == 1) ? PIC1_DATA : PIC2_DATA;
outb(port, mask);
}
// MARK :- Helpers
void pic_send_eoi(uint8_t irq)
{
if (irq >= 8) {
outb(PIC2_COMMAND, PIC_EOI);
}
printf("Sending EOI for IRQ: %d, EOI: %x, to CMD: %x\n", irq, PIC_EOI, PIC1_COMMAND);
// Always signal PIC1 that an interrupt has been handled
// because it's the PIC that forwards PIC2's irqs as well.
outb(PIC1_COMMAND, PIC_EOI);
}
static uint16_t pic_get_irq_reg(int ocw3)
{
/* OCW3 to PIC CMD to get the register values. PIC2 is chained, and
* represents IRQs 8-15. PIC1 is IRQs 0-7, with 2 being the chain */
outb(PIC1_COMMAND, ocw3);
outb(PIC2_COMMAND, ocw3);
return (inb(PIC2_COMMAND) << 8) | inb(PIC1_COMMAND);
}
/* Returns the combined value of the cascaded PICs irq request register */
static uint16_t pic_get_irr(void)
{
return pic_get_irq_reg(PIC_READ_IRR);
}
/* Returns the combined value of the cascaded PICs in-service register */
static uint16_t pic_get_isr(void)
{
return pic_get_irq_reg(PIC_READ_ISR);
}
I don't see any keyboard ISR code, and that could well be where your problem is. I'm guessing your are using the PC/AT style interface to interact with the KB controller (ports 60, 61, 64 if I remember correctly). There are one or more OUTs to one (or more) of these ports before they'll generate another interrupt - if memory serves.
In general any hardware device will require attention after it generates an interrupt. The PIC generating a time tick interrupt is an exception.
In the way of advice, I have a few suggestions:
Inspect the compiler code that you think is incorrectly trashing the AX register and/or the register_t structure (I'm not entirely sure from the OP which you are saying is being trashed). Identify exactly how you think the compiler is incorrect. I suspect the compiler is not incorrect, but rather that it is just tricky to call a C function from an ISR. In doing that in the past I had to go through a number of iterations to get it right.
Come up with a debugging scheme where you can view recent events.
a. One method is to use the fixed locations in the VGA screen memory (I'm assuming you are working in text mode at this point) where you can write single characters that indicate what your program is doing. A few characters at the end of the top line in particular, let's say 4. The 4 characters represent the 4 most recent events. When an event (an interrupt or a call from a user mode program to the kernel as examples) occurs you move the 3 most recent chars to the left (or right) and then put the newest char in the "most recent" slot. When you're program is working normally you'll see those locations on the screen as kind of a blur as they rapidly get updated due to various events. However if your program hangs the characters will stop changing and you'll see the 4 most recent events, even if you can't break in with a debugger.
b. Create a circular in memory trace log. Each entry in the log represents an event but can include details about the event like register values, or program state. If you break in with a debugger you can view the log as either bytes, words, dwords, and decode the events in your head. Or if you have a debugger that allows custom extensions, then write an extensions that supports displaying and querying the log for events of interest.
FWIW, I implemented a V86 monitor (old Intel terminology for virtualized real mode hypervisor) and so these suggestions come from experience. These suggestions apply to dev testing your kernel on both bare metal and to a somewhat lesser extent using a VM as your dev test platform.
With a VM and the right hypervisor, you will have access to a debugger specifically designed to debug a VM. Virtual Box, for example, has such a debugger. Windbg can be used on Hyper-V, although it was very slow when I tried it last. However someone wrote an extension that made kernel debugging of Hyper-V VMs much quicker.
Related
Hyper-V output:
Code:
/*PIC Definition*/
#define PIC1 0x20 /* IO base address for master PIC */
#define PIC2 0xA0 /* IO base address for slave PIC */
#define PIC1_COMMAND PIC1
#define PIC1_DATA (PIC1+1)
#define PIC2_COMMAND PIC2
#define PIC2_DATA (PIC2+1)
#define ICW1_ICW4 0x01 /* ICW4 (not) needed */
#define ICW1_SINGLE 0x02 /* Single (cascade) mode */
#define ICW1_INTERVAL4 0x04 /* Call address interval 4 (8) */
#define ICW1_LEVEL 0x08 /* Level triggered (edge) mode */
#define ICW1_INIT 0x10 /* Initialization - required! */
#define ICW4_8086 0x01 /* 8086/88 (MCS-80/85) mode */
#define ICW4_AUTO 0x02 /* Auto (normal) EOI */
#define ICW4_BUF_SLAVE 0x08 /* Buffered mode/slave */
#define ICW4_BUF_MASTER 0x0C /* Buffered mode/master */
#define ICW4_SFNM 0x10 /* Special fully nested (not) */
#define inb(x,y) asm volatile ("inb %1, %0" : "=a"(x) : "d"(y));
void PIC_remap(BYTE offset1, BYTE offset2)
{
unsigned char a1, a2, cmd;
WORD portnum = PIC1_DATA;
inb(a1, portnum); // save masks
portnum = PIC2_DATA;
inb(a2, portnum);
WORD ret1 = a1, ret2 = a2;
printf("Response from PIC1 and PIC2: %d %d", ret1, ret2);
portnum = PIC1_COMMAND;
cmd = (ICW1_INIT | ICW1_ICW4);
outb(portnum, cmd); // starts the initialization sequence (in cascade mode)
io_wait();
portnum = PIC2_COMMAND;
outb(portnum, cmd);
io_wait();
portnum = PIC1_DATA;
outb(portnum, offset1); // ICW2: Master PIC vector offset
io_wait();
portnum = PIC2_DATA;
outb(portnum, offset2); // ICW2: Slave PIC vector offset
io_wait();
portnum = PIC1_DATA;
cmd = 4;
outb(portnum, cmd); // ICW3: tell Master PIC that there is a slave PIC at IRQ2 (0000 0100)
io_wait();
portnum = PIC2_DATA;
cmd = 2;
outb(portnum, cmd); // ICW3: tell Slave PIC its cascade identity (0000 0010)
io_wait();
portnum = PIC1_DATA;
cmd = ICW4_8086;
outb(portnum, cmd);
io_wait();
portnum = PIC2_DATA;
cmd = ICW4_8086;
outb(portnum, cmd);
io_wait();
outb(PIC1_DATA, a1); // restore saved masks.
outb(PIC2_DATA, a2);
}
I am doing some search on how the programmable interrupt controller behaves under the real mode. But I came up with some problems.
Expected behavior: PIC1 and PIC2 should return 0 0.
Reality: They return 184 (0xB8) and 15 (0xF). Can any one tell me why?
Reference: https://wiki.osdev.org/8259_PIC
Zack (The OP) provided an update with their implementation of inb and this answer has been modified to provide a more specific answer. inb has been defined as a macro this way:
#define inb(x,y) asm volatile ("inb %1, %0" : "=a"(x) : "d"(y));
It might be clearer to name macros with upper case identifiers like INB. It was unclear to me from the original code that this was a macro at all. An alternative to a macro would be to make inb a static inline function in a shared header file. You could even mark it with __attribute__((always_inline)) so that it gets inlined on lower optimization levels. The function could have been defined this way:
typedef unsigned short int WORD;
typedef unsigned char BYTE;
#define alwaysinline __attribute__((always_inline))
static inline alwaysinline BYTE inb (WORD portnum)
{
BYTE byteread;
asm volatile ("inb %1, %0"
: "=a"(byteread)
: "Nd"(portnum));
return byteread;
}
Values Returned by PIC DATA Port
When you read from the PIC DATA port you will be reading the current mask values that are used to determine which interrupts will cause the CPU to be interrupted. It effectively determines which specific interrupts are enabled and disabled on each PIC. A bit value of 0 represents enabled and 1 represents disabled.
If both values are 0 then all the interrupts on both PIC1 and PIC2 will be enabled. This may be the case in some environments, but it doesn't necessarily have to be the case. What you read may be non zero and indicative of what interrupts the BIOS enabled and disabled.
According to the screenshot PIC1 has the value 184 (binary 10111000) and PIC2 is 15 (binary 00001111). If these are in fact the values read from the PICs then it would suggest the BIOS/Firmware enabled these interrupts (and the rest disabled):
IRQ0 - Timer
IRQ1 - Keyboard
IRQ2 - Cascade interrupt
IRQ6 - Usually the Floppy controller
IRQ12 - Mouse/PS2
IRQ13 - Inter Processor interrupt (IPI) - in old days it was for the separate FPU
IRQ14 - Primary ATA Channel (HDD/CD-ROM etc)
IRQ15 - Secondary ATA Channel (HDD/CD-ROM etc)
These make sense given they are the common interrupts that you would expect would be present on a system that was supporting legacy BIOS and devices. Although your values are not zero, they do in fact look reasonable and are likely the real values read from both PICs
After a major refactoring of an embedded system (IAR C on TI CC2530), I've ended up in the following situation:
After basic initialization of peripherals and global interrupt enable, the execution incorrectly ends up in an interrupt handler that communicates with external hardware. Since this hardware is not ready (remember, we end up in the ISR incorrectly), the program freezes triggering a watchdog reset.
If I insert 1, 2, 3, 5, 6, 7 etc NOPs in main(), everything works fine.
But If I insert 0, 4, 8 etc NOPs, I get the faulty behaviour.
CC2530 fetches 4 bytes of instructions from flash memory, on 4-byte boundaries.
This tells me that something is misaligned when it comes to code memory, but I simply doesn't know where to start. Nothing has changed when it comes the target settings AFAIK.
Anyone here who has seen this situation before, or can point me in the right direction?
#include <common.h>
#include <timer.h>
#include <radio.h>
#include <encryption.h>
#include "signals.h"
#include "lock.h"
#include "nfc.h"
#include "uart1_trace.h"
#include "trace.h"
//------------------------------------------------------------------------------
// Public functions
//------------------------------------------------------------------------------
void main(void)
{
setTp;
// Initialize microcontroller and peripherals
ClockSourceInit();
WatchdogEnable();
PortsInit();
TraceInit();
Timer4Init();
SleepInit();
RadioInit();
Uart1Init();
LoadAesKey("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
clrTp;
NfcInit();
__enable_interrupt();
asm("nop");
// Initialize threads
LockInit();
while (true)
{
WDR();
LockRun();
}
}
void NfcInit(void)
{
// Enable wake up interrupt on external RF field present
// The process for enabling interrupts is described in section 2.5.1 in the CC2530 datasheet.
// Configure interrupt source: interrupt on falling edge, Port 0, pin 7:0
PICTL |= BIT(0);
// 1. Clear port 0 individual interrupt flag. Read-modify-write is not allowed.
// Writing 1 to a bit in this register has no effect, so 1 should be written to flags that are not to be cleared.
P0IFG = ~BIT(3);
// Clear port 0 interrupt flag. This register is bit-accessible.
P0IF = 0;
// 2. Set pin 3 interrupt-enable
P0IEN |= BIT(3);
// 3. Set port 0 interrupt-enable
IEN1 |= BIT(5);
// 4. Global interrupt enable is set in main()
}
// Interrupt handler: falling edge on signal Wake.
// This interrupt will only occur when device is powered off and NFC field present.
// When device is powered on, VCORE is always asserted.
#pragma vector = 0x6B
__interrupt static void NFC_WAKE_ISR(void)
{
static uint16 cnt = 0;
TracePutUint16(cnt); TracePuts("\r\n");
if (++cnt > 10)
IEN1 &= ~BIT(5);
P0IFG = ~BIT(3); // Clear port 1 individual interrupt flag. Read-modify-write is not allowed.
P0IF = 0; // Clear port 1 CPU interrupt flag. This register is bit-accessible.
return;
Screenshot of software init.
CH1 = External interrupt signal, active low (signal Wake).
CH2 = TP in main.c (setTp / clrTp).
The reset button on CC-debugger seems not to be debounced, so the TP signal turns on and off a few times before stabilizing (should not be an issue). VCC is stable long before the reset. When TP goes low for the last time, all peripherals are initialized.
An external NFC IC is used to wake up the MCU from sleep mode when a NFC field is present. The NFC IC is powered by one of the CC2530 I/O-pins. Normally the IC is powered off to preserve power. In this state, the energy from the NFC field is enough to generate the wake signal (active low). When this signal is detected by the MCU, it wakes up, applies power to the NFC IC, and NFC communication starts.
The NFC IC generates the signal either when powered, or when a NFC field is present.
After reset, all I/O-pins are configured as inputs with pullups. This pulled up input is enough to power the NFC IC, which is why the wake-signal is generated. Immediatly after reset, the I/O is configured (in function PortsInit()), and power to NFC IC is turned off. This makes the wake signal go low. The slow rise- and fall times are probably due to a capacitor, that I will now remove.
Here is where things get weird. Despite the wake signal being low, the external interrupt is configured for falling edge and pending int flag is cleared right before global in enabled, I end up in the ISR a few ms later (not seen in the screen shot). But only with the right number of NOPs, as described above.
If I add a > 15 ms delay before global int enable, all is fine. This coincides with the time measured from TP low to wake high.
One might think that the int is incorrectly configured for active low, but in that case I should get multiple ints, and I don't. Also, that does not explain the magic NOPs...
Compiler generated ISR assembly code:
// 77 // Interrupt handler: falling edge on signal Wake.
// 78 // This interrupt will only occur when device is powered off and NFC field present.
// 79 // When device is powered on, VCORE is always asserted.
// 80 #pragma vector = 0x6B
RSEG NEAR_CODE:CODE:NOROOT(0)
// 81 __interrupt static void NFC_WAKE_ISR(void)
NFC_WAKE_ISR:
// 82 {
PUSH A
MOV A,#-0xe
LCALL ?INTERRUPT_ENTER_XSP
; Saved register size: 15
; Auto size: 0
// 83 static uint16 cnt = 0;
// 84
// 85 TracePutUint16(cnt); TracePuts("\r\n");
; Setup parameters for call to function PutUint16
MOV R4,#(TPutc & 0xff)
MOV R5,#((TPutc >> 8) & 0xff)
MOV DPTR,#??cnt
MOVX A,#DPTR
MOV R2,A
INC DPTR
MOVX A,#DPTR
MOV R3,A
LCALL PutUint16
; Setup parameters for call to function TPuts
MOV R2,#(`?<Constant "\\r\\n">` & 0xff)
MOV R3,#((`?<Constant "\\r\\n">` >> 8) & 0xff)
LCALL TPuts
// 86
// 87 if (++cnt > 10)
MOV DPTR,#??cnt
MOVX A,#DPTR
ADD A,#0x1
MOV R0,A
INC DPTR
MOVX A,#DPTR
ADDC A,#0x0
MOV R1,A
MOV DPTR,#??cnt
MOV A,R0
MOVX #DPTR,A
INC DPTR
MOV A,R1
MOVX #DPTR,A
CLR C
MOV A,R0
SUBB A,#0xb
MOV A,R1
SUBB A,#0x0
JC ??NFC_WAKE_ISR_0
// 88 IEN1 &= ~BIT(5);
CLR 0xb8.5
// 89
// 90
// 91 P0IFG = ~BIT(3); // Clear port 1 individual interrupt flag. Read-modify-write is not allowed.
??NFC_WAKE_ISR_0:
MOV 0x89,#-0x9
// 92 P0IF = 0; // Clear port 1 CPU interrupt flag. This register is bit-accessible.
CLR 0xc0.5
// 93
// 94 return;
MOV R7,#0x1
LJMP ?INTERRUPT_LEAVE_XSP
REQUIRE _A_P0
REQUIRE P0IFG
REQUIRE _A_P1
REQUIRE _A_IEN1
REQUIRE _A_IRCON
////////////////////////////////////////////////////////////////////////////////
// lnk51ew_CC2530F64.xcl: linker command file for IAR Embedded Workbench IDE
// Generated: Mon May 24 00:00:01 +0200 2010
//
////////////////////////////////////////////////////////////////////////////////
//
// Segment limits
// ==============
//
// IDATA
// -----
-D_IDATA0_START=0x00
-D_IDATA0_END=0xFF
//
// PDATA
// -----
// We select 256 bytes of (I)XDATA memory that can be used as PDATA (see also "PDATA page setup" below)
-D_PDATA0_START=0x1E00
-D_PDATA0_END=0x1EFF
//
//
// IXDATA
// ------
-D_IXDATA0_START=0x0001 // Skip address 0x0000 (to avoid ambiguities with NULL pointer)
-D_IXDATA0_END=0x1EFF // CC2530F64 has 8 kB RAM (NOTE: 256 bytes are used for IDATA)
//
//
// XDATA
// -----
-D_XDATA0_START=_IXDATA0_START
-D_XDATA0_END=_IXDATA0_END
//
// NEAR CODE
// ---------
-D_CODE0_START=0x0000
-D_CODE0_END=0xFFFF // CC2530F64 has 64 kB code (flash)
//
// Special SFRs
// ============
//
// Register bank setup
// -------------------
-D?REGISTER_BANK=0x0 // Sets default register bank (0,1,2,3)
-D_REGISTER_BANK_START=0x0 // Start address for default register bank (0x0, 0x8, 0x10, 0x18)
//
// PDATA page setup
// ----------------
-D?PBANK_NUMBER=0x1E // High byte of 16-bit address to the PDATA area
//
// Virtual register setup
// ----------------------
-D_BREG_START=0x00
-D?VB=0x20
-D?ESP=0x9B //Extended stack pointer register location
////////////////////////////////////////////////////////////////////////////////
//
// IDATA memory
// ============
-Z(BIT)BREG=_BREG_START
-Z(BIT)BIT_N=0-7F
-Z(DATA)REGISTERS+8=_REGISTER_BANK_START
-Z(DATA)BDATA_Z,BDATA_N,BDATA_I=20-2F
-Z(DATA)VREG+_NR_OF_VIRTUAL_REGISTERS=08-7F
-Z(DATA)PSP,XSP=08-7F
-Z(DATA)DOVERLAY=08-7F
-Z(DATA)DATA_I,DATA_Z,DATA_N=08-7F
-U(IDATA)0-7F=(DATA)0-7F
-Z(IDATA)IDATA_I,IDATA_Z,IDATA_N=08-_IDATA0_END
-Z(IDATA)ISTACK+_IDATA_STACK_SIZE#08-_IDATA0_END
-Z(IDATA)IOVERLAY=08-FF
//
// ROM memory
// ==========
//
// Top of memory
// -------------
-Z(CODE)INTVEC=0
-Z(CODE)CSTART=_CODE0_START-_CODE0_END
//
// Initializers
// ------------
-Z(CODE)BIT_ID,BDATA_ID,DATA_ID,IDATA_ID,IXDATA_ID,PDATA_ID,XDATA_ID=_CODE0_START-_CODE0_END
//
// Program memory
// --------------
-Z(CODE)RCODE,DIFUNCT,CODE_C,CODE_N,NEAR_CODE=_CODE0_START-_CODE0_END
//
// Checksum
// --------
-Z(CODE)CHECKSUM#_CODE0_END
//
// XDATA memory
// ============
//
// Stacks located in XDATA
// -----------------------
-Z(XDATA)EXT_STACK+_EXTENDED_STACK_SIZE=_EXTENDED_STACK_START
-Z(XDATA)PSTACK+_PDATA_STACK_SIZE=_PDATA0_START-_PDATA0_END
-Z(XDATA)XSTACK+_XDATA_STACK_SIZE=_XDATA0_START-_XDATA0_END
//
// PDATA - data memory
// -------------------
-Z(XDATA)PDATA_Z,PDATA_I=_PDATA0_START-_PDATA0_END
-P(XDATA)PDATA_N=_PDATA0_START-_PDATA0_END
//
// XDATA - data memory
// -------------------
-Z(XDATA)IXDATA_Z,IXDATA_I=_IXDATA0_START-_IXDATA0_END
-P(XDATA)IXDATA_N=_IXDATA0_START-_IXDATA0_END
-Z(XDATA)XDATA_Z,XDATA_I=_XDATA0_START-_XDATA0_END
-P(XDATA)XDATA_N=_XDATA0_START-_XDATA0_END
-Z(XDATA)XDATA_HEAP+_XDATA_HEAP_SIZE=_XDATA0_START-_XDATA0_END
-Z(CONST)XDATA_ROM_C=_XDATA0_START-_XDATA0_END
//
// Core
// ====
-cx51
////////////////////////////////////////////////////////////////////////////////
//
// Texas Instruments device specific
// =================================
//
// Flash lock bits
// ---------------
//
// The CC2530 has its flash lock bits, one bit for each 2048 B flash page, located in
// the last available flash page, starting 16 bytes from the page end. The number of
// bytes with flash lock bits depends on the flash size configuration of the CC2530
// (maximum 16 bytes, i.e. 128 page lock bits, for the CC2530 with 256 kB flash).
// Note that the bit that controls the debug interface lock is always in the last byte,
// regardless of flash size.
//
-D_FLASH_LOCK_BITS_START=(_CODE0_END-0xF)
-D_FLASH_LOCK_BITS_END=_CODE0_END
//
// Define as segment in case one wants to put something there intentionally (then comment out the trick below)
-Z(CODE)FLASH_LOCK_BITS=_FLASH_LOCK_BITS_START-_FLASH_LOCK_BITS_END
//
// Trick to reserve the FLASH_LOCK_BITS segment from being used as normal CODE, avoiding
// code to be placed on top of the flash lock bits. If code is placed on address 0x0000,
// (INTVEC is by default located at 0x0000) then the flash lock bits will be reserved too.
//
-U(CODE)0x0000=(CODE)_FLASH_LOCK_BITS_START-_FLASH_LOCK_BITS_END
//
////////////////////////////////////////////////////////////////////////////////
According to TI, that part has got an 8051 core. Apart from being dinosaur crap, 8051 is an 8-bitter so alignment does not apply.
When random modifications to the code result in completely unrelated errors or run-away code, it is most often caused by one of these things:
You got a stack overflow, or
You got undefined behavior bugs, such as uninitialized variables, array out of bounds access etc.
Also ensure that all ISRs are registred in the interrupt vector table.
EDIT after question change 6/4:
You should normally not return from interrupts! I don't know how your specific setup works, but with a general embedded systems compiler, the non-standard interrupt keyword means two things:
Ensure that the calling convention upon entering the ISR is correct, by stacking whatever registers the CPU/ABI state are not stacked by hardware, but software.
Ensure that the same registers are restored upon leaving the ISR and that the correct return instruction is used.
On 8051 this means that the disassembled ISR absolutely must end with a RETI instruction and not a RET instruction! Chances are high that return results in RET which will sabotage your stack. Disassemble to see if this is indeed the case.
The user's guide for the CC2530 states:
The instruction that sets the PCON.IDLE bit must be aligned in a
certain way for correct operation. The first byte of the assembly
instruction immediately following this instruction must not be placed
on a 4-byte boundary.
This is likely why the system fails on NOP multiples of four.
Just below the warning, there is an implementation for fixing this alignment specifically targeted at the IAR compiler.
I am starting to believe that this is a hardware issue, related to the connection between CC2530 and the NFC IC.
The power and reset to the NFC IC that sends the external interrupt request is controlled by a CC2530 I/O pin with 20 mA current drive capacity. At reset, before execution of the program starts, all the I/O pins defaults to inputs with internal weak pull-up. It seems like the current through the pull-up resistor is enough to power up the NFC IC. The interrupt signal from the NFC IC is high whenever the NFC is powered or a NFC field is present, and inverted by a FET transistor before reaching CC2530. Hence the ISR is triggered by a falling edge on the input.
So what happens at startup is that the NFC IC is incorrectly powered on (and later off, when the ports are initialized), and the WAKE signal falls and rises very slowly due to the poor drive capacity of a pull-up (to make things worse, a large capacitor of 1 uF is connected in parallel with the gate of the FET, and another 1uF filters the NFC IC power pin).
WAKE is supposed to trigger an interrupt only on falling edge, but staying in the transition region for up to 10 ms as seen in the oscilloscope screenshot above seems to cause CC2530 to fire the interrupt even when WAKE is rising. The ISR starts to communicate with the NFC IC via SPI, but at this time, the NFC IC seems to be messed up due to the spurious transitions on VCC and reset. It refuses to respond, the execution halts in the ISR and the watchdog bites. And the process starts over, forever.
When I insert a delay that ensures WAKE to be stable high before enabling the interrupt, all is well.
If I remove the 1 uF cap on the FET gate, WAKE rises very quickly, and there is no need for the delay anymore. And when I add a 4k7 pulldown to the NFC power, it is no longer powered up at reset.
Problem seems to be solved. The refactoring rearranged the code and changed the startup sequence, which led to a different delay that revealed the issue. With the proper hardware update, no delay will be needed.
But what still disturbes me is that I don't understand the magic NOPs. When CC2530 had the interrupt enabled and encountered a slowly rising WAKE, it wouldn't always end up incorrectly in the ISR. And when it did, I could always make it run by adding 1..3 NOPs. Naturally, whenever I added or removed a line of code, the number of NOPs required changed, which as you can imagine, drove me crazy.
It took me some time to narrow things down, and I am very grateful to all your comments and proposed solutions, especially Clifford that forced me to bring out the oscilloscope.
I'm working on a simple kernel and I've been trying to implement a keyboard interrupt handler to get rid of port polling. I've been using QEMU in -kernel mode (to reduce compile time, because generating the iso using grub-mkrescue takes quite some time) and it worked just fine, but when I wanted to switch to -cdrom mode it suddenly started crashing. I had no idea why.
Eventually I've realized that when it boots from an iso it also runs a GRUB bootloader before booting the kernel itself. I've figured out GRUB probably switches the processor into protected mode and that causes the problem.
the problem:
Normally I'd simply initialize the interrupt handler and whenever I'd press a key it would be handled. However when I run my kernel using an iso and pressed a key the virtual machine simply crashed. This happened in both qemu and VMWare so I assume there must be something wrong with my interrupts.
Bear in mind that the code work just fine for as long as I don't use GRUB.
interrupts_init()(see below) is one of the first things called in the main() kernel function.
Essentially the question is: Is there a way to make this work in protected mode?.
A complete copy of my kernel can be found in my GitHub repository. Some relevant files:
lowlevel.asm:
section .text
global keyboard_handler_int
global load_idt
extern keyboard_handler
keyboard_handler_int:
pushad
cld
call keyboard_handler
popad
iretd
load_idt:
mov edx, [esp + 4]
lidt [edx]
sti
ret
interrupts.c:
#include <assembly.h> // defines inb() and outb()
#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1
extern void keyboard_handler_int(void);
extern void load_idt(void*);
struct idt_entry
{
unsigned short int offset_lowerbits;
unsigned short int selector;
unsigned char zero;
unsigned char flags;
unsigned short int offset_higherbits;
} __attribute__((packed));
struct idt_pointer
{
unsigned short limit;
unsigned int base;
} __attribute__((packed));
struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;
void load_idt_entry(int isr_number, unsigned long base, short int selector, unsigned char flags)
{
idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
idt_table[isr_number].selector = selector;
idt_table[isr_number].flags = flags;
idt_table[isr_number].zero = 0;
}
static void initialize_idt_pointer()
{
idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
idt_ptr.base = (unsigned int)&idt_table;
}
static void initialize_pic()
{
/* ICW1 - begin initialization */
outb(PIC_1_CTRL, 0x11);
outb(PIC_2_CTRL, 0x11);
/* ICW2 - remap offset address of idt_table */
/*
* In x86 protected mode, we have to remap the PICs beyond 0x20 because
* Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
*/
outb(PIC_1_DATA, 0x20);
outb(PIC_2_DATA, 0x28);
/* ICW3 - setup cascading */
outb(PIC_1_DATA, 0x00);
outb(PIC_2_DATA, 0x00);
/* ICW4 - environment info */
outb(PIC_1_DATA, 0x01);
outb(PIC_2_DATA, 0x01);
/* Initialization finished */
/* mask interrupts */
outb(0x21 , 0xFF);
outb(0xA1 , 0xFF);
}
void idt_init(void)
{
initialize_pic();
initialize_idt_pointer();
load_idt(&idt_ptr);
}
void interrupts_init(void)
{
idt_init();
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);
/* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
outb(0x21 , 0xFD);
}
kernel.c
#if defined(__linux__)
#error "You are not using a cross-compiler, you will most certainly run into trouble!"
#endif
#if !defined(__i386__)
#error "This kernel needs to be compiled with a ix86-elf compiler!"
#endif
#include <kernel.h>
// These _init() functions are not in their respective headers because
// they're supposed to be never called from anywhere else than from here
void term_init(void);
void mem_init(void);
void dev_init(void);
void interrupts_init(void);
void shell_init(void);
void kernel_main(void)
{
// Initialize basic components
term_init();
mem_init();
dev_init();
interrupts_init();
// Start the Shell module
shell_init();
// This should be unreachable code
kernel_panic("End of kernel reached!");
}
boot.asm:
bits 32
section .text
;grub bootloader header
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero
global start
extern kernel_main
start:
mov esp, stack_space ;set stack pointer
call kernel_main
; We shouldn't get to here, but just in case do an infinite loop
endloop:
hlt ;halt the CPU
jmp endloop
section .bss
resb 8192 ;8KB for stack
stack_space:
I had a hunch last night as to why loading through GRUB and loading through the Multiboot -kernel feature of QEMU might not work as expected. That is captured in the comments. I have managed to confirm the findings based on more of the source code being released by the OP.
In the Mulitboot Specification there is a note about the GDTR and the GDT with regards to modifying selectors that is relevant:
GDTR
Even though the segment registers are set up as described above, the ‘GDTR’ may be invalid, so the OS image must not load any segment registers (even just reloading the same values!) until it sets up its own ‘GDT’.
An interrupt routine could alter the CS selector causing issues.
There is another concern and most likely the root cause of problems. The Multiboot specification also states this about the selectors it creates in its GDT:
‘CS’
Must be a 32-bit read/execute code segment with an offset of ‘0’ and a
limit of ‘0xFFFFFFFF’. The exact value is undefined.
‘DS’
‘ES’
‘FS’
‘GS’
‘SS’
Must be a 32-bit read/write data segment with an offset of ‘0’ and a limit
of ‘0xFFFFFFFF’. The exact values are all undefined.
Although it says what types of descriptors will be set up it doesn't actually specify that a descriptor has to have a particular index. One Multiboot loader may have a Code segment descriptor at index 0x08 and another bootloader may use 0x10. This is of particular relevance when you look at one line of your code:
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);
This creates an IDT descriptor for interrupt 0x21. The third parameter 0x08 is the Code selector the CPU needs to use to access the interrupt handler. I discovered this works on QEMU where the code selector is 0x08, but in GRUB it appears to be 0x10. In GRUB the 0x10 selector points at a non-executable Data segment and this will not work.
To get around all these problems the best thing to do is set up your own GDT shortly after starting up your kernel and before setting up an IDT and enabling interrupts. There is a tutorial on the GDT in the OSDev Wiki if you want more information.
To set up a GDT I'll simply create an assembler routine in lowlevel.asm to do it by adding a load_gdt function and data structures:
global load_gdt
; GDT with a NULL Descriptor, a 32-Bit code Descriptor
; and a 32-bit Data Descriptor
gdt_start:
gdt_null:
dd 0x0
dd 0x0
gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:
; GDT descriptor record
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
; Load GDT and set selectors for a flat memory model
load_gdt:
lgdt [gdt_descriptor]
jmp CODE_SEG:.setcs ; Set CS selector with far JMP
.setcs:
mov eax, DATA_SEG ; Set the Data selectors to defaults
mov ds, eax
mov es, eax
mov fs, eax
mov gs, eax
mov ss, eax
ret
This creates and loads a GDT that has a NULL Descriptor at index 0x00, a 32-bit code descriptor at 0x08, and a 32-bit data descriptor at 0x10. Since we are using 0x08 as the code selector this matches what you specify as a code selector in your IDT entry initialization for interrupt 0x21:
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);
The only other thing is that you'll need to amend your kernel.c to call load_gdt. One can do that with something like:
extern void load_gdt(void);
void kernel_main(void)
{
// Initialize basic components
load_gdt();
term_init();
mem_init();
dev_init();
interrupts_init();
// Start the Shell module
shell_init();
// This should be unreachable code
kernel_panic("End of kernel reached!");
}
I'm trying to program a very simple kernel for learning purposes. After reading a bunch of articles about the PIC and IRQs in the x86 architecture,
I've figured out that IRQ1 is the keyboard handler. I'm using the following code to print the keys being pressed:
#include "port_io.h"
#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1
void keyboard_handler();
void load_idt(void*);
struct idt_entry
{
unsigned short int offset_lowerbits;
unsigned short int selector;
unsigned char zero;
unsigned char flags;
unsigned short int offset_higherbits;
};
struct idt_pointer
{
unsigned short limit;
unsigned int base;
};
struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;
void load_idt_entry(char isr_number, unsigned long base, short int selector, char flags)
{
idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
idt_table[isr_number].selector = selector;
idt_table[isr_number].flags = flags;
idt_table[isr_number].zero = 0;
}
static void initialize_idt_pointer()
{
idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
idt_ptr.base = (unsigned int)&idt_table;
}
static void initialize_pic()
{
/* ICW1 - begin initialization */
write_port(PIC_1_CTRL, 0x11);
write_port(PIC_2_CTRL, 0x11);
/* ICW2 - remap offset address of idt_table */
/*
* In x86 protected mode, we have to remap the PICs beyond 0x20 because
* Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
*/
write_port(PIC_1_DATA, 0x20);
write_port(PIC_2_DATA, 0x28);
/* ICW3 - setup cascading */
write_port(PIC_1_DATA, 0x00);
write_port(PIC_2_DATA, 0x00);
/* ICW4 - environment info */
write_port(PIC_1_DATA, 0x01);
write_port(PIC_2_DATA, 0x01);
/* Initialization finished */
/* mask interrupts */
write_port(0x21 , 0xff);
write_port(0xA1 , 0xff);
}
void idt_init()
{
initialize_pic();
initialize_idt_pointer();
load_idt(&idt_ptr);
}
load_idt just uses the lidt x86 instruction. Afterwards I'm loading the keyboard handler:
void kmain(void)
{
//Using grub bootloader..
idt_init();
kb_init();
load_idt_entry(0x21, (unsigned long) keyboard_handler, 0x08, 0x8e);
}
This is the implementation:
#include "kprintf.h"
#include "port_io.h"
#include "keyboard_map.h"
void kb_init(void)
{
/* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
write_port(0x21 , 0xFD);
}
void keyboard_handler(void)
{
unsigned char status;
char keycode;
char *vidptr = (char*)0xb8000; //video mem begins here.
/* Acknownlegment */
int current_loc = 0;
status = read_port(0x64);
/* Lowest bit of status will be set if buffer is not empty */
if (status & 0x01) {
keycode = read_port(0x60);
if(keycode < 0)
return;
vidptr[current_loc++] = keyboard_map[keycode];
vidptr[current_loc++] = 0x07;
}
write_port(0x20, 0x20);
}
This is the extra code I'm using:
section .text
global load_idt
global keyboard_handler
extern kprintf
extern keyboard_handler_main
load_idt:
sti
mov edx, [esp + 4]
lidt [edx]
ret
global read_port
global write_port
; arg: int, port number.
read_port:
mov edx, [esp + 4]
in al, dx
ret
; arg: int, (dx)port number
; int, (al)value to write
write_port:
mov edx, [esp + 4]
mov al, [esp + 4 + 4]
out dx, al
ret
This is my entry point:
bits 32
section .text
;grub bootloader header
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero
global start
extern kmain
start:
; cli ;block interrupts
mov esp, stack_space ;set stack pointer
call kmain
hlt ;halt the CPU
section .bss
resb 8192 ;8KB for stack
stack_space:
I'm using QEMU to run the kernel:
qemu-system-i386 -kernel kernel
The problem is that I'm not getting any character on the screen. Instead, I still get the same output:
SeaBIOS (version Ubuntu-1.8.2-1-ubuntu1)
Booting from ROM...
How do I solve this problem? Any suggestions?
You have a number of issues with your code. The main ones are discussed individually below.
The HLT instruction will halt the current CPU waiting for the next interrupt. You do have interrupts enabled by this point. After the first interrupt (keystroke) the code after HLT will be executed. It will start executing whatever random data is in memory. You could modify your kmain to do an infinite loop with a HLT instruction. Something like this should work:
while(1) __asm__("hlt\n\t");
In this code:
load_idt:
sti
mov edx, [esp + 4]
lidt [edx]
ret
It is generally a better idea to use STI after you update the interrupt table, not before it. This would be better:
load_idt:
mov edx, [esp + 4]
lidt [edx]
sti
ret
Your interrupt handler needs to perform an iretd to properly return from an interrupt. Your function keyboard_handler will do a ret to return. To resolve this you could create an assembly wrapper that calls the C keyboard_handler function and then does an IRETD.
In a NASM assembly file you could define a global function called keyboard_handler_int like this:
extern keyboard_handler
global keyboard_handler_int
keyboard_handler_int:
call keyboard_handler
iretd
The code to setup the IDT entry would look like this:
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);
Your kb_init function eventually enables (via a mask) the keyboard interrupt. Unfortunately, you set up the keyboard handler after you enable that interrupt. It is possible for a keystroke to be pressed after the interrupt is enabled and before the entry is placed in the IDT. A quick fix is to set your keyboard handler up before the call to kb_init with something like:
void kmain(void)
{
//Using grub bootloader..
idt_init();
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);
kb_init();
while(1) __asm__("hlt\n\t");
}
The most serious problem that is likely causing your kernel to triple fault (and effectively rebooting the virtual machine) is the way you defined the idt_pointer structure. You used:
struct idt_pointer
{
unsigned short limit;
unsigned int base;
};
The problem is that default alignment rules will place 2 bytes of padding after limit and before base so that the unsigned int will be aligned at a 4 byte offset within the structure. To alter this behavior and pack the data without padding, you can use __attribute__((packed)) on the structure. The definition would look like this:
struct idt_pointer
{
unsigned short limit;
unsigned int base;
} __attribute__((packed));
Doing it this way means that there are no extra bytes placed between limit and base for alignment purposes. Failure to deal with the alignment issue effectively yields a base address that is incorrectly placed in the structure. The IDT pointer needs a 16-bit value representing the size of the IDT followed immediately by a 32-bit value representing the base address of your IDT.
More information on structure alignment and padding can be found in one of Eric Raymond's blogs. Because of the way that members of struct idt_entry are placed there are no extra padding bytes. If you are creating structs that you never want padded I recommend using __attribute__((packed));. This is generally the case when you are mapping a C data structure with a system defined structure. With that in mind I'd also pack struct idt_entry for clarity.
Other considerations
In the interrupt handler, although I suggested an IRETD, there is another issue. As your kernel grows and you add more interrupts you'll discover another problem. Your kernel may act erratically and registers may change values unexpectedly. The issue is that C functions acting as interrupt handlers will destroy the contents of some registers, but we don't save and restore them. Secondly, the direction flag (per the 32-bit ABI) is required to be cleared (CLD) before a function is called. You can't assume the direction flag is cleared upon entry to the interrupt routine. The ABI says:
EFLAGS The flags register contains the system flags, such as the direction
flag and the carry flag. The direction flag must be set to the
‘‘forward’’ (that is, zero) direction before entry and upon exit
from a function. Other user flags have no specified role in the
standard calling sequence and are not preserved
You could push all the volatile registers individually but for brevity you can use the PUSHAD and POPAD instructions. An interrupt handler would be better if it looked like:
keyboard_handler_int:
pushad ; Push all general purpose registers
cld ; Clear direction flag (forward movement)
call keyboard_handler
popad ; Restore all general purpose registers
iretd ; IRET will restore required parts of EFLAGS
; including the direction flag
If you were to save and restore all the volatile registers manually you'd have to save and restore EAX, ECX, and EDX as they don't need to be preserved across C function calls. It generally isn't a good idea to to use x87 FPU instructions in an interrupt handler (mostly for performance), but if you did you'd have to save and restore the x87 FPU state as well.
Sample Code
You didn't provide a complete example, so I filled in some of the gaps (including a simple keyboard map) and slight change to your keyboard handler. The revised keyboard handler only displays key down events and skips over characters that had no mapping. In all cases the code drops through to the end of the handler so that the PIC is sent an EOI (End Of Interrupt). The current cursor location is a static integer that will retain its value across interrupt calls. This allows the position to advance between each character press.
My kprintd.h file is empty, and I put ALL the assembler prototypes into your port_io.h. The prototypes should be divided properly into multiple headers. I only did it this way to reduce the number of files. My file lowlevel.asm defines all the low level assembly routines. The final code is as follows:
kernel.asm:
bits 32
section .text
;grub bootloader header
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero
global start
extern kmain
start:
lgdt [gdtr] ; Load our own GDT, the GDTR of Grub may be invalid
jmp CODE32_SEL:.setcs ; Set CS to our 32-bit flat code selector
.setcs:
mov ax, DATA32_SEL ; Setup the segment registers with our flat data selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, stack_space ; set stack pointer
call kmain
; If we get here just enter an infinite loop
endloop:
hlt ; halt the CPU
jmp endloop
; Macro to build a GDT descriptor entry
%define MAKE_GDT_DESC(base, limit, access, flags) \
(((base & 0x00FFFFFF) << 16) | \
((base & 0xFF000000) << 32) | \
(limit & 0x0000FFFF) | \
((limit & 0x000F0000) << 32) | \
((access & 0xFF) << 40) | \
((flags & 0x0F) << 52))
section .data
align 4
gdt_start:
dq MAKE_GDT_DESC(0, 0, 0, 0); null descriptor
gdt32_code:
dq MAKE_GDT_DESC(0, 0x00ffffff, 10011010b, 1100b)
; 32-bit code, 4kb gran, limit 0xffffffff bytes, base=0
gdt32_data:
dq MAKE_GDT_DESC(0, 0x00ffffff, 10010010b, 1100b)
; 32-bit data, 4kb gran, limit 0xffffffff bytes, base=0
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1
; limit (Size of GDT - 1)
dd gdt_start ; base of GDT
CODE32_SEL equ gdt32_code - gdt_start
DATA32_SEL equ gdt32_data - gdt_start
section .bss
resb 8192 ; 8KB for stack
stack_space:
lowlevel.asm:
section .text
extern keyboard_handler
global read_port
global write_port
global load_idt
global keyboard_handler_int
keyboard_handler_int:
pushad
cld
call keyboard_handler
popad
iretd
load_idt:
mov edx, [esp + 4]
lidt [edx]
sti
ret
; arg: int, port number.
read_port:
mov edx, [esp + 4]
in al, dx
ret
; arg: int, (dx)port number
; int, (al)value to write
write_port:
mov edx, [esp + 4]
mov al, [esp + 4 + 4]
out dx, al
ret
port_io.h:
extern unsigned char read_port (int port);
extern void write_port (int port, unsigned char val);
extern void kb_init(void);
kprintf.h:
/* Empty file */
keyboard_map.h:
unsigned char keyboard_map[128] =
{
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', /* 9 */
'9', '0', '-', '=', '\b', /* Backspace */
'\t', /* Tab */
'q', 'w', 'e', 'r', /* 19 */
't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', /* Enter key */
0, /* 29 - Control */
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', /* 39 */
'\'', '`', 0, /* Left shift */
'\\', 'z', 'x', 'c', 'v', 'b', 'n', /* 49 */
'm', ',', '.', '/', 0, /* Right shift */
'*',
0, /* Alt */
' ', /* Space bar */
0, /* Caps lock */
0, /* 59 - F1 key ... > */
0, 0, 0, 0, 0, 0, 0, 0,
0, /* < ... F10 */
0, /* 69 - Num lock*/
0, /* Scroll Lock */
0, /* Home key */
0, /* Up Arrow */
0, /* Page Up */
'-',
0, /* Left Arrow */
0,
0, /* Right Arrow */
'+',
0, /* 79 - End key*/
0, /* Down Arrow */
0, /* Page Down */
0, /* Insert Key */
0, /* Delete Key */
0, 0, 0,
0, /* F11 Key */
0, /* F12 Key */
0, /* All other keys are undefined */
};
keyb.c:
#include "kprintf.h"
#include "port_io.h"
#include "keyboard_map.h"
void kb_init(void)
{
/* This is a very basic keyboard initialization. The assumption is we have a
* PS/2 keyboard and it is already in a proper state. This may not be the case
* on real hardware. We simply enable the keyboard interupt */
/* Get current master PIC interrupt mask */
unsigned char curmask_master = read_port (0x21);
/* 0xFD is 11111101 - enables only IRQ1 (keyboard) on master pic
by clearing bit 1. bit is clear for enabled and bit is set for disabled */
write_port(0x21, curmask_master & 0xFD);
}
/* Maintain a global location for the current video memory to write to */
static int current_loc = 0;
/* Video memory starts at 0xb8000. Make it a constant pointer to
characters as this can improve compiler optimization since it
is a hint that the value of the pointer won't change */
static volatile char *const vidptr = (char*)0xb8000;
void keyboard_handler(void)
{
signed char keycode;
keycode = read_port(0x60);
/* Only print characters on keydown event that have
* a non-zero mapping */
if(keycode >= 0 && keyboard_map[keycode]) {
vidptr[current_loc++] = keyboard_map[keycode];
/* Attribute 0x07 is white on black characters */
vidptr[current_loc++] = 0x07;
}
/* Send End of Interrupt (EOI) to master PIC */
write_port(0x20, 0x20);
}
main.c:
#include "port_io.h"
#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1
void keyboard_handler_int();
void load_idt(void*);
struct idt_entry
{
unsigned short int offset_lowerbits;
unsigned short int selector;
unsigned char zero;
unsigned char flags;
unsigned short int offset_higherbits;
} __attribute__((packed));
struct idt_pointer
{
unsigned short limit;
unsigned int base;
} __attribute__((packed));
struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;
void load_idt_entry(int isr_number, unsigned long base, short int selector, unsigned char flags)
{
idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
idt_table[isr_number].selector = selector;
idt_table[isr_number].flags = flags;
idt_table[isr_number].zero = 0;
}
static void initialize_idt_pointer()
{
idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
idt_ptr.base = (unsigned int)&idt_table;
}
static void initialize_pic()
{
/* ICW1 - begin initialization */
write_port(PIC_1_CTRL, 0x11);
write_port(PIC_2_CTRL, 0x11);
/* ICW2 - remap offset address of idt_table */
/*
* In x86 protected mode, we have to remap the PICs beyond 0x20 because
* Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
*/
write_port(PIC_1_DATA, 0x20);
write_port(PIC_2_DATA, 0x28);
/* ICW3 - setup cascading */
write_port(PIC_1_DATA, 0x04);
write_port(PIC_2_DATA, 0x02);
/* ICW4 - environment info */
write_port(PIC_1_DATA, 0x01);
write_port(PIC_2_DATA, 0x01);
/* Initialization finished */
/* mask interrupts */
write_port(0x21 , 0xff);
write_port(0xA1 , 0xff);
}
void idt_init()
{
initialize_pic();
initialize_idt_pointer();
load_idt(&idt_ptr);
}
void kmain(void)
{
//Using grub bootloader..
idt_init();
load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);
kb_init();
while(1) __asm__("hlt\n\t");
}
In order to link this kernel I use a file link.ld with this definition:
/*
* link.ld
*/
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
. = 0x100000;
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
.bss : { *(.bss) }
}
I compile and link this code using a GCC i686 cross compiler with these commands:
nasm -f elf32 -g -F dwarf kernel.asm -o kernel.o
nasm -f elf32 -g -F dwarf lowlevel.asm -o lowlevel.o
i686-elf-gcc -g -m32 -c main.c -o main.o -ffreestanding -O3 -Wall -Wextra -pedantic
i686-elf-gcc -g -m32 -c keyb.c -o keyb.o -ffreestanding -O3 -Wall -Wextra -pedantic
i686-elf-gcc -g -m32 -Wl,--build-id=none -T link.ld -o kernel.elf -ffreestanding -nostdlib lowlevel.o main.o keyb.o kernel.o -lgcc
The result is a kernel called kernel.elf with debug information. I prefer an optimization level of -O3 rather than a default of -O0. Debug information makes it easier to debug with QEMU and GDB. The kernel can be debugged with these commands:
qemu-system-i386 -kernel kernel.elf -S -s &
gdb kernel.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout regs' \
-ex 'break kmain' \
-ex 'continue'
If you wish to debug at the assembly code level replace layout src with layout asm. When run with the input the quick brown fox jumps over the lazy dog 01234567890 QEMU displayed this:
I am developing a kernel from scratch in C. I am having a problem with the keyboard. In Qemu, when I press a key on my keyboard, the keypress is handled normally. When I run it in VirtualBox or on an actual computer, it works fine until I press a key. In virtualbox, when it crashes, it gives me the error code VINF_EM_TRIPLE_FAULT. Here is my (important parts) of the code:
Assembly:
bits 32
section .text
;multiboot spec
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00)
global start
global keyboard_handler
global read_port
global write_port
global load_idt
extern main ;this is defined in the c file
extern keyboard_handler_main
read_port:
mov edx, [esp + 4]
;al is the lower 8 bits of eax
in al, dx ;dx is the lower 16 bits of edx
ret
write_port:
mov edx, [esp + 4]
mov al, [esp + 4 + 4]
out dx, al
ret
load_idt:
mov edx, [esp + 4]
lidt [edx]
sti ;turn on interrupts
ret
keyboard_handler:
call keyboard_handler_main
iretd
start:
cli ;block interrupts
mov esp, stack_space
call main
hlt ;halt the CPU
section .bss
resb 8192; 8KB for stack
stack_space:
C (The important parts, I omitted parts that don't involve the keyboard):
struct IDT_entry{
unsigned short int offset_lowerbits;
unsigned short int selector;
unsigned char zero;
unsigned char type_attr;
unsigned short int offset_higherbits;
};
struct IDT_entry IDT[IDT_SIZE];
void idt_init(void)
{
unsigned long keyboard_address;
unsigned long idt_address;
unsigned long idt_ptr[2];
/* populate IDT entry of keyboard's interrupt */
keyboard_address = (unsigned long)keyboard_handler;
IDT[0x21].offset_lowerbits = keyboard_address & 0xffff;
IDT[0x21].selector = KERNEL_CODE_SEGMENT_OFFSET;
IDT[0x21].zero = 0;
IDT[0x21].type_attr = INTERRUPT_GATE;
IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16;
/* Ports
* PIC1 PIC2
*Command 0x20 0xA0
*Data 0x21 0xA1
*/
/* ICW1 - begin initialization */
write_port(0x20 , 0x11);
write_port(0xA0 , 0x11);
/* ICW2 - remap offset address of IDT */
/*
* In x86 protected mode, we have to remap the PICs beyond 0x20 because
* Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
*/
write_port(0x21 , 0x20);
write_port(0xA1 , 0x28);
/* ICW3 - setup cascading */
write_port(0x21 , 0x00);
write_port(0xA1 , 0x00);
/* ICW4 - environment info */
write_port(0x21 , 0x01);
write_port(0xA1 , 0x01);
/* Initialization finished */
/* mask interrupts */
write_port(0x21 , 0xff);
write_port(0xA1 , 0xff);
/* fill the IDT descriptor */
idt_address = (unsigned long)IDT ;
idt_ptr[0] = (sizeof (struct IDT_entry) * IDT_SIZE) + ((idt_address & 0xffff) << 16);
idt_ptr[1] = idt_address >> 16 ;
load_idt(idt_ptr);
}
void kb_init(void)
{
/* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
write_port(0x21 , 0xFD);
}
void keyboard_handler_main(void) {
unsigned char status;
char keycode;
/* write EOI */
write_port(0x20, 0x20);
status = read_port(KEYBOARD_STATUS_PORT);
/* Lowest bit of status will be set if buffer is not empty */
if (status & 0x01) {
keycode = read_port(KEYBOARD_DATA_PORT);
if(keycode < 0){
return;
}
char c = keyboard_map[(unsigned char) keycode]; //keyboard_map converts key codes to ASCII
(prints the char)
}
}
}
void main(void)
{
idt_init();
kb_init();
while(1);
print("Welcome to Code OS");
}
And then of course they are compiled with a linker.
So why is this working in qemu but not an actual computer? (On an actual computer it works fine until I press any key.)