MicroBeast IRQ rabbit-hole

MicroBeast IRQ rabbit-hole
Photo by Sincerely Media / Unsplash

Recently there was an innocuous comment on the MicroBeast discord from our Great Leader:

which led me down a bit of a rabbit-hole.

Why's it hard?

It's hard because of a well-documented z80 shortcoming: if an LD A,I or LD A,R is itself interrupted, the P/V flag is reset, even if interrupts were enabled.

Surely this is a solved problem?

It is: the canonical approach is:

( from here) which is clumsy, but works reliably. Tuna2020 is not a fan, his proposal is:

his argument being that he controls the BIOS and he controls the IRQ handler, so all should be well.

But for me, this just raises more questions!

Go on then...

  1. why do we have a ZX spectrum style 257-byte IRQ handler table, when all our interrupts come through a lovely Z84C20 PIO controller that has all the plumbing to vector directly to a 16 bit address?
  2. the UART has an interrupt line that is already wired to port B of the PIO - what's that all about?
  3. This is an experimenters' machine: the IO ports A and B are deliberately exposed for people to tinker with, and the included example code talks about interrupts - it's not unreasonable that someone would try to install their own interrupt handler. How hard is it to do that?
  4. If they do, and they attach a Port A interrupt handler, Tuna2020's solution will not be aware of it, and will return the wrong result non-deterministically.
  5. If they do, and they attach a Port B interrupt handler, and are graceful enough to return via the system interrupt handler, it will over-count clock ticks. Anything relying on that to be a metronomic regular pulse (like a game loop) is going to be in for a wild, unpredictable (and pontentially highly accelerated) ride.

...and?

  1. turns out, there's an INT line on the RC2014 connector: interrupts can come from that bus, and they might not be "nice" (spectrum-style writing any old crap to D[7:0] when the interrupt is asserted). So it's defensive. Also, the BIOS doesn't set up the PIO interrupt vectors at all so on any given boot the PIO ends up calling a random entry in this table: so the 257 entries are essential, it turns out. A happy side effect: it's quite easy for a user to patch this table to hook their own ISRs...more on this later.
  2. Unclear. It is wired in to PB4. Its interrupt is even enabled in the PIO. But a conflicting comment in the BIOS code says it isn't: I suspect this is a hangover from a time when the UART was meant to be interrupt driven, but that never happened, or it got backed out for some reason. Interrupts are not enabled at source in the UART itself. So this means the UART would be an excellent source of spurious interrupts should we need one. More on this later...
  3. Not that hard! More on this later...
  4. Still the case, and not a lot the user can do short of:
write your own operating system, or completely ditch the BIOS
  1. Should be easily demonstrable. More on this later...

what did you do?

My thinking was, the UART interrupt is already wired. I need to reconfigure the PIO to treat it as a second source of interrupts, enable the interrupt in the UART itself, send loads of characters at 19200 baud, generating interrupts at (19200/10=1920 Hz) so I'd be incrementing the system tick handler at 30x normal speed.

IN the event this was very easy, because the PIO plumbing for the UART had already been done. UART config was all we needed.

This looked like a job for BBC BASIC: as it offers a unique blend of easy-access machine code thanks to its built in assembler, and floating point division for our rate calculations.

The code has two passes: the first pass is a loop, in each loop iteration it measures how many clock ticks have passed since the previous iteration. At the end it computes the average.

In the second pass, it does the same thing again, but this time with interrupts enabled. The idea is, we'd see an increase in the overall rate because the system is now fooled into believing that every received character is a tick of its 64 Hz real-time clock, in addition to legitimate interrupts from that clock source.

We'd write a new ISR (shim in the code) that clears the UART interrupt and calls the system ISR; patching it in at a known address in the system interrupt table: we know what address to write because we can do the missing PIO Port B interrupt vector setup ourselves (enable in the code).

My first attempt is in irqs.basic. I tested it by simply holding down a key for the duration of pass 2. This gave a clock speedup of about 1.157: significant but not earth-shattering.

I surmised that this is the keyboard auto-repeat rate, so I tried again, this time disconnecting the terminal at the right juncture and using:

cat /dev/urandom > /dev/ttyUSB0 

to generate traffic at full tilt.

This time I got a speed-up of ... 0.257. That's right, more interrupts made it slower! Turns out the system interrupt handler is doing quite a lot of work, and the additional latency from all those new interrupts is really taking a toll.

So, next I wrote a python script for the PC side that sends characters at a nice predictable 64 chars/sec.

This time I got a speedup of 2.5 which was slightly over the expected 2x, but close enough for jazz.

Next I updated the python script to make the character sending rate variable, and changed the Beast code to not call the system interrupt handler: we'd just increment its 32-bit tick counter for it, and avoid all that tedious mucking about with keyboards and consoles.

Sending at 256 characters/second, I measured a speedup of 8.31 - again slightly over.

I got cocky at this stage and ported the whole timing loop to assembler, just leaving the floating point division to BASIC (see irqs3.bas). This is where the wheels come off: the measurement routine returns 0 most of the time, and it isn't obvious to me what the issue is... If you spot anything give me a shout!

so what did we learn?

We learned that you can install your own interrupt handlers very easily in BeastOS.

We also learned that if you do, there are consequences:

  1. if your interrupt is for Port B of the PIO, you're going to banjax the system clock. You could mitigate this by detecting the source of interrupt and calling the system IRQ handler only if the source really was the RTC interrupt.
  2. If your interrupt is other (PIO Port A, some RC2014 peripheral that generates interrupts correctly ) then you don't have the clock problem, but the BIOS' interrupt-state-preserving measures will be compromised. It will return bogus results, non-deterministically.
  3. As Tuna2020 says, you're probably better off rolling your own at this point. Then you're not fighting the BIOS' interrupt handler (and you probably don't want all the latency it introduces doing all the keyboard and console handling anyway).

Fun times!