Close

Hardware Timers, Vectored Interrupts, Exception Handling in C

A project log for Mackerel 68k Computer

A computer engineering journey through the Motorola 68k CPU family

colin-maykishColin Maykish 02/27/2022 at 01:100 Comments

With the MFP serial port working reliably, the next requirement for a barebones Linux system is a constant timer to allow the kernel to do context switching and multitasking. It's fortunate, then, that the MFP also provides four independent timers which can trigger interrupts on the 68008 CPU. It can also be configured to trigger interrupts for other reasons (serial port data available, GPIO input levels changing, etc.), but for now, a constant timer will be a big step forward toward the main goal of booting Linux.

Vectored Interrupts

The MC68000 series of CPUs supports vectored interrupts. It’s worth taking a minute to understand what that means. It took me a few days to really internalize this idea. In most 8-bit CPUs from a generation before the 68k, interrupts are not vectored, but polled. For example, when a peripheral wants to interrupt the 6502 CPU, it asserts the IRQ pin. The CPU stops what it was doing, stores some internal state to the stack, and then calls the interrupt handling code. Since there’s only one interrupt pin, all of the peripherals have to share. So when the CPU is ready to handle the interrupt, it has to ask each peripheral if it was the cause of the interrupt and then choose the appropriate action to take when the source is found. This is simple to design on the hardware side since every peripheral can connect in parallel to the IRQ pin, but it means the interrupt handling code is more complicated and potentially slower.

Vectored interrupts, on the other hand, require more hardware, but they provide a way for the CPU to know exactly where each interrupt comes from without having to ask each potential source. On the 68k, this is done with an asynchronous interrupt system that prioritizes interrupts according to level.

Instead of one IRQ pin, there are three IPL inputs representing a binary number from 0 to 7. These are the interrupt levels where 0 means no interrupt and 7 is a non-maskable interrupt. The minimum interrupt level can be set in software and when the CPU detects an interrupt at or above its minimum level, it will start the interrupt handling process. As with polled interrupts, it will finish executing the current instruction, store some state to the stack, and then find the appropriate handler function to call. The main advantage, though, over polled interrupts is that instead of asking every peripheral, the CPU expects the location of the interrupt handler to be exposed on the CPU bus by the peripheral. In other words, when the CPU acknowledges that it’s ready to handle the interrupt, the peripheral that made the request tells the CPU which handler to call. This has to be set up ahead of time in code of course, but this one-time setup simplifies and speeds up interrupt handling for the lifetime of the program. It also means interrupt handlers can be changed programmatically, which gives a lot of flexibility to the code.

There are some details I left out (that will come up in the implementation section), but this is the description of vectored interrupts I wish I had when I started working on this. I hope it’s clear enough to be helpful to someone in the same position.

Design Changes and More Logic To Implement

The first step to getting interrupts going is to bring in some additional control lines into play. The MFP has an IRQ output that must be mapped to one of the 7 interrupt levels on the CPU. Since it is currently the only source of interrupts, the IRQ pin is connected directly to IPL2. All three IPL pins have a pullup resistor, so this means the MFP will cause a level 4 interrupt. If more than three interrupt levels are ever needed, something like a 3-to-8 decoder would be necessary to use the whole range.

There are three pins on the 68008, FC0 through FC2, that, when all high, indicate the CPU is acknowledging an interrupt. This will be called the /IACK signal. It is used internally on the CPLD, but also connected to the MFP’s IACK pin. When the CPU is ready to handle an interrupt, it will alert the MFP with this signal.

assign IACK = ~(FC0 & FC1 & FC2);

The MFP then sends the interrupt vector to the CPU and asserts DTACK to say that it’s done. Now the CPU knows which function to call.

This interrupt acknowledgement cycle looks like this:

Interrupt acknowledgement cycle
1) IPL2, 2) IACK, 3) DTACK, 4) AS

One interesting detail about the interrupt process on the MFP is that the chip-enable signal does not get selected again once the interrupt starts. The process is ended when DTACK is asserted, so now the DTACK logic must also consider the IACK signal. The normal logic of DTACK remains, but I have added another way for it to be asserted when IACK is active:

assign DTACK = (MFPEN & DTACK_MFP & ~IACK) | (~MFPEN & DTACK_MFP & IACK);

In other words, If IACK is not asserted, let the MFP control DTACK normally. If IACK is asserted, let the MFP control it even though the MFP will not be selected by the address decoder.

Exception Handling Without Assembly Code

One of my goals for this project is to avoid writing large parts of the code in assembly. I have nothing against assembly and the 68k assembly language seems to be one of the nicer ones to write, but I am much more productive in a C environment.

All of my monitor code and test programs have been in C so far, but I was not exactly sure how to handle interrupts without dropping into assembly. In a lot of basic 68k systems, the vector table is either stored in ROM at the start of memory, or something like a BOOT signal is used and the vector table from ROM gets copied into RAM at the same location. Either way, this is generally defined in assembly.

Fortunately, there are two things that make exception/interrupt handling doable almost entirely in C. The first one being that the m68k version of gcc allows a function to be tagged as an interrupt handler so the appropriate assembly can be generated (i.e. ending with a call to rte instead of rts). It looks like this:

void __attribute__((interrupt)) MFPTimerBTick()

The second thing is just having the vector table in RAM, which I realize is obviously the intended place for it, but it means that the vector table is easily modifiable with some pointer manipulation in C. Picking a vector number and setting the memory value to a function pointer is simple. I wrote a small wrapper function to set up such a mapping:

void set_exception_handler(unsigned char exception_number, void (*exception_handler)())
{
   *((int *)(exception_number * 4)) = (int)exception_handler;
}

As long as the function pointer points to an interrupt handler, this will work exactly the same as defining vectors in assembly.

In the startup code, every vector is set up to point to a common “unhandled exception” function. Application code can then implement and map new handlers as needed.

void __attribute((interrupt)) unhandled_exception()
{
   // Display a pattern on the MFP LEDs and loop forever
   MEM(MFP_GPDR) = 0xAA;

   while (1)
   {
   }
}

void _start()
{
   // Disable interrupts
   set_interrupts(false);

   // Initialize the vector table at the start of RAM
   for (int i = 0; i < VECTOR_TABLE_SIZE; i += 1)
   {
       // All exceptions start off pointing to a known handler
       set_exception_handler(i, &unhandled_exception);
   }

   // Enable interrupts
   set_interrupts(true);

   // Setup the hardware peripherals
   mfp_init();

   // Call main
   main();
}

You may notice the set_interrupts function. This is actually the only code where I could not avoid assembly. To enable and disable interrupts globally, the status register has to be modified. This is done with some inline assembly in C, a decent compromise in my opinion.

void set_interrupts(bool enabled)
{
   if (enabled)
   {
       // Set minimum interrupt level to 0 in the status register
       asm("and.w #0xF8FF, %sr");
   }
   else
   {
       // Set minimum interrupt level to 7, i.e. only non-maskable interrupts enabled
       asm("or.w #0x700, %sr");
   }
}

A Working System Timer

Finally, with all of this in place, I can set up one of the MFP timers to generate steady interrupts and have them do something useful without stopping the main application loop.

// Map an exception handler for the MFP timer B interrupt
set_exception_handler(0x48, &MFPTimerBTick);

// Set MFP Timer B to run at 36 Hz and trigger an interrupt on every tick
MEM(MFP_TBDR) = 0;         // Timer B counter max (i.e 255);
MEM(MFP_TBCR) = 0b0010111; // Timer B enabled, delay mode, /200 prescalar
MEM(MFP_VR) = 0x40;        // Set base vector for MFP interrupt handlers
MEM(MFP_IERA) = 0x01;      // Enable interrupts for Timer B
MEM(MFP_IMRA) = 0x01;      // Unmask interrupt for Timer B

In this test, the main application loop is outputting a steady stream of characters on the serial port, while the interrupt is incrementing the value of the GPIO register, which shows up as counting LEDs.

Interrupt-driven LEDs
MFP timer interrupts driving counting LEDs
Main application loop test
Main application loop writing letters to the serial console

Hardware timers are obviously useful for all sorts of applications, but having a simple system interrupt timer is one step closer to the big Linux milestone.

Discussions