Building a hardware interrupt controller

I’ve recently been adding simple hardware devices to my home-built 6502 computer, and ran into a problem.

The 6502 has two active-low interrupt inputs (IRQ and NMI), both of which are used in my design. If I add any devices which can trigger their own interrupts, I will need a way to combine multiple interrupt signals into one.

Choosing an approach

I researched some retro computer designs to see how this problem was handled, and found some very simple approaches.

Most commonly, multiple I/O chips would be connected to the 6502’s IRQ line, which is level-triggered and active-low. As long as all connected chips had an open-drain IRQ output, they could be connected in a so-called wired-or configuration:

In software, each interrupt source would be checked in priority order.

I can’t use something so straightforward to add hardware to my design, for two main reasons:

  • I would need to use logic gates to combine the inputs instead. The I/O chip which is connected to the CPU’s IRQ input at the moment is a WDC 65C22S, which does not have an open-drain IRQ output.
  • It’s not going to be practical to check every I/O device from an interrupt service routine. I’m planning to add devices which are accessed over SPI, and will take many clock cycles to return their status.

Over in the PC world, programmable interrupt controllers such as the Intel 8259 were used. Among other features, these chips combine multiple interrupt sources into one, and can report the interrupt source on the data bus.

Rather than use out-of-production retro parts, I decided to program an ATF22V10 programmable logic device with these functions. A PLD is cheaper, and I’ve just figured out how to program them, so I might as well put those skills to use.

Creating the interrupt controller

I am using galette to program these PLD’s, and went through 5 revisions of the PLD source file before landing on something which could complete these functions.

  • combine multiple interrupt lines into one.
  • allow the CPU to quickly identify the highest-priority interrupt to service.

For this section, I’ll briefly describe each part of the PLD definition is doing.

The file starts with the name of the target device, and a name for the chip.


Next pin definitions are listed. I’m using the ATF22V10, which has 24 pins. The first row is pins 1-12, while the second row is pins 13-24. Numbers go left-to-right in both rows, unlike a physical microchip!

Clock    /IRQ0 /IRQ1 IRQ2  /IRQ3 /IRQ4 IRQ5  /IRQ6 /IRQ7 /IRQ8 /IRQ9  GND
/CS      D7    D6    D5    D4    D3    D2    D1    D0    /IRQ  /WE    VCC

For combining the interrupts, I simply use a big OR function.

IRQ = IRQ0 + IRQ1 + IRQ2 + IRQ3 + IRQ4 + IRQ5 + IRQ6 + IRQ7 + IRQ8 + IRQ9

This reads “IRQ is active if IRQ0 is active or IRQ1 is active or IRQ2 is active, etc.”. All logic is the positive case, so “IRQ0” means “IRQ0 is active”. Whether “active” means 1 or 0 at the input depends on those pin definitions. The active-low inputs are inverted before being fed to this expression, and the result will be inverted if the output is active-low. This confused me at first, because I thought that “/IRQ0” is just a pin name.

The expressions for the data bus come next.

  • These are all ‘registered’ outputs (indicated with the .R suffix). This runs the output through a D-type flip-flop, so that it will only change on clock transitions, rather than asynchronously. This is to avoid garbage output if an interrupt triggers while we are reading from the controller.
  • D1..D4 is a priority encoder, encoding the number 0 to 10 to identify the lowest-numbered interrupt source, or 11 if there is no interrupt.
  • D0 is always 0. This multiples the output by 2, which makes things easier for the software.
D0.R = GND
D1.R = IRQ1*/IRQ0 + IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ9*/IRQ8*/IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0
D2.R = IRQ2*/IRQ1*/IRQ0 + IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + /IRQ9*/IRQ8*/IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0
D3.R = IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0 + IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0
D4.R = /IRQ7*/IRQ6*/IRQ5*/IRQ4*/IRQ3*/IRQ2*/IRQ1*/IRQ0
D5.R = GND
D6.R = GND
D7.R = GND

For the 22V10, not all pins support the same number of product terms. As I built up to 10 interrupt sources, I needed to re-arrange the pins a few times.

Lastly, I needed to tri-state the output when the chip was not being read. This is a second definition for each pin (.E suffix). I used both a chip select (/CS) and write enable (/WE) input. The interrupt controller cannot be written to, this just allows me to avoid bus contention if somebody is attempting to.

D0.E = CS*/WE
D1.E = CS*/WE
D2.E = CS*/WE
D3.E = CS*/WE
D4.E = CS*/WE
D5.E = CS*/WE
D6.E = CS*/WE
D7.E = CS*/WE

Test circuit and software

After testing everything on breadboard, I connected the new interrupt controller to my 6502 computer. All of the required signals are exposed via pin headers on my 6502 computer, more information about that can be found on previous blog posts.

This is how it looks. It’s not the neatest, but at least I don’t have the whole computer on breadboards anymore.

I then started writing some 6502 assembly code to test this out. I’ve been working on a shell which runs named commands, so I added a command called irqtest. This code references other parts of the ROM, which can be found in the GitHub repository for this project.

This code sets up a timer to trigger an interrupt on the 65C22 VIA, then uses the wai instruction to wait for interrupts. When the interrupt service routine completes, the code resumes. I’ve assigned two addresses (DEBUG_LAST_INTERRUPT_INDEX and DEBUG_INTERRUPT_COUNT) so that we can check that the correct interrupt routines are running.

; Set up a timer to trigger an IRQ

; Some values to help us debug

  lda #$ff              ; set interrupt index to dummy value (so we can see if it's not being overridden)
  lda #$00              ; reset interrupt counter
  sta $01
  ; setup for via
  lda #%00000000        ; set ACR. first two bits = 00 is one-shot for T1
  sta VIA_ACR
  lda #%11000000        ; enable VIA interrupt for T1
  sta VIA_IER
  sei                   ; enable IRQ at CPU - normally off in this code
  ; set up a timer at ~65535 clock pulses.
  lda #$ff              ; set T1 low-order counter
  sta VIA_T1C_L
  lda #$ff              ; set T1 high-order counter
  sta VIA_T1C_H
  wai                   ; wait for interrupt
  ; reset for via
  cli                   ; disable IRQ at CPU - normally off in this code
  lda #%01000000        ; disable VIA interrupt for T1
  ; Print out which interrupt was used, should be 02 if irq1_isr ran
  jsr hex_print_byte
  jsr shell_newline
  ; print number of times interrupt ran, should be 01 if it only ran once
  jsr hex_print_byte
  jsr shell_newline
  lda #0
  jmp sys_exit

I set my interrupt service routine read from the interrupt controller, then jump to the correct routine.

  phx                       ; push x for later
  inc DEBUG_INTERRUPT_COUNT ; count how many times this runs..
  ldx IRQ_CONTROLLER        ; read interrupt controller to find highest-priority interrupt to service
  jmp (isr_jump_table, X)   ; jump to matching service routine

  plx                       ; restore x

The interrupt controller can return 11 possible values, so I made an 11-entry table, with the address for each interrupt handler (2 bytes each). I have connected the VIA to IRQ1, so I set the second entry to a subroutine called irq1_isr.

isr_jump_table:              ; 10 possible interrupt sources
.word nop_isr
.word irq1_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr
.word nop_isr               ; 11th option for when no source is triggering the interrupt (phantom interrupt)

Most of the interrupt routines map to nothing at all. If this were a real OS, an interrupt from an unknown source would need to be a fatal error, since it can’t be individually masked/ignored on this hardware.

nop_isr:                         ; interrupt routine for anything else
  stx DEBUG_LAST_INTERRUPT_INDEX ; store interrupt index for debugging
  jmp irq_return

When IRQ1 is triggered though, this routine will clear the interrupt on the VIA. If we fail to do this, then the CPU will become stuck, processing interrupts forever.

irq1_isr:                        ; interrupt routine for VIA
  stx DEBUG_LAST_INTERRUPT_INDEX ; store interrupt index for debugging
  ldx VIA_T1C_L                  ; clear IFR bit 6 on VIA (side-effect of reading T1 low-order counter)
  jmp irq_return

Once I fixed some bugs, I was able to get the expected output, which is 02 (the index we are using to jump into isr_jump_table), followed by 01 (the number of times the interrupt service routine runs).

While I was researching this, I also found that online 6502 assembly guides often suggest clearing the interrupt flag on I/O chips near the start of the interrupt service routine, apparently to avoid running the routine twice. From this test, I know that the interrupt service routine is only running once, so I am happily disregarding that advice. Thinking about it, I can only see this applying to open-drain IRQ outputs.

Note about KiCad symbols

Since my last blog post about PLD’s, I’ve started to create separate symbols in KiCad for each programmed chip. You can see one of these in the schematic above.

I’m producing these automatically. Galette has a .pin file as one of its outputs. The file for this chip is as follows:

 Pin # | Name     | Pin Type
   1   | Clock    | Clock/Input
   2   | /IRQ0    | Input
   3   | /IRQ1    | Input
   4   | IRQ2     | Input
   5   | /IRQ3    | Input
   6   | /IRQ4    | Input
   7   | IRQ5     | Input
   8   | /IRQ6    | Input
   9   | /IRQ7    | Input
  10   | /IRQ8    | Input
  11   | /IRQ9    | Input
  12   | GND      | GND
  13   | /CS      | Input
  14   | D7       | Output
  15   | D6       | Output
  16   | D5       | Output
  17   | D4       | Output
  18   | D3       | Output
  19   | D2       | Output
  20   | D1       | Output
  21   | D0       | Output
  22   | /IRQ     | Output
  23   | /WE      | Input
  24   | VCC      | VCC

To produce schematic symbols, I wrote a small Python script to convert this text format to a CSV file, suitable for KiPart.



KiPart ships with profiles for FPGA chips, but the generic input worked fine for the ATF22V10 PLD.

./KiPart/venv/bin/kipart -r generic --overwrite irq_controller.csv

The output is a .lib file, which I can include in any KiCad project.


This approach works, and introduces far less overhead compared with checking each device in the interrupt service routine. In particular, it will allow me to test interrupts from slow-to-access SPI devices.

I will be disconnecting this this for now, but may include it in an expansion board or updated computer design if I ever make one!

Leave a Reply

Your email address will not be published. Required fields are marked *