Close

RC 2022/10 Day 6: Flounder Interrupted

A project log for Flounder Z180 Computer

Standalone single-board computer based on the Zilog Z180 CPU

colin-maykishColin Maykish 10/07/2022 at 03:160 Comments

ASCI Clock Generation

I've been cheating a little in the way I use the Z180's onboard serial ports, specifically the baud rate generation. In a typical application, the baud rate is generated in the CPU as some factor of the system clock PHI. It's a little tricky to understand the exact combination of register bits required to make this happen, so my "cheat" was to not to use it at all. Instead, it's possible to provide a second oscillator to the CPU along with the system clock. This oscillator can be used directly as the baudrate (divided by 16). In my case, an oscillator of 1.8432 MHz on the CKA pin will give a baudrate of 115,200 without any additional software configuration. This is fine, but it does mean a second oscillator is required and the baudrate is not easily adjustable. I figured it was time to generate the ASCI clock properly and eliminate the extra hardware.

Somewhat annoyingly, the internal clock generators still require a magic oscillator frequency, but it's more flexible in which magic oscillator you pick. 18.432 MHz (yes, 10x the other one) seems like the most common choice since it's close to the speed limit for the 20 MHz Z180 variant and the 33 MHz variant can apparently handle doubling it to 36ish MHz without issue.

To actually generate an ASCI clock from the system clock, there are a few key registers. I'll outline my configuration as an example:

CCR = 0x00: This sets PHI to 1/2 of CLK, i.e. 9.216 MHz
ASEXT0 = 0x00: Set the X1 and BRG mode bits to 0
CNTLB0 = 0b00100001: Set the PS bit to 1 for to /30, DR bit to 0 for /16 and SS to 0b001 for /2

All of that should come out to: 18.432 MHz / 2 / 30 / 16 / 2 = 9600 baud. To adjust the baud rate, these factors can be scaled up or down as necessary.

Periodic System Timer

The next thing I needed to tackle was interrupts. I've implemented interrupts on the 6502 and the 68000. The Z180 is somewhere in the middle in terms of complexity. There are three interrupt modes that the CPU can be in at any one time. Mode 0 and 1 seem to be mostly for compatibility with older or non-Z80 specific peripherals. Mode 2 is the focus today: vectored interrupts.

Similar to the 68000, when a mode 2 interrupt is triggered, the CPU basically asks the peripheral for the address of its interrupt handler and then jumps there to execute the interrupt service routine. The Z180 has four external interrupt pins, but I've chosen to ignore those for now (they're actually disabled entirely) and focus on using the internal countdown timers to generate a periodic interrupt.

Conceptually, this means I need to set up the timer peripheral, write an interrupt service routine somewhere in code, link the timer interrupt with the ISR, and then enable interrupts. In reality, this was a lot harder than I expected. The Z180 manuals and documentation I could find were light on details for actually implementing something like this and the z88dk wiki is tough to follow when you're using custom hardware and not one of their supported systems. I'm confident there are better and more robust ways to implement vectored interrupts under the z88dk umbrella, but my first goal is always to get anything working and then iterate.  In that spirit, here's the bare minimum code I set up to get a periodic timer interrupt running:

    org $0000
    jp init

; Block 0x00 - 0xFF is reserved for other interrupt modes

; Block 0x100 - 0x1FF is reserved for interrupt vector map

ALIGN 0x104

    defw asm_isr_prt0       ; Put the address of the PRT0 timer ISR at the right vector

; Code definition starts at 0x200

ALIGN 0x200

init:
    di
    im 2

    ld sp, $FFFF            ; Set stack pointer to top of RAM

    ld a, $01
    ld I, a                 ; Set base interrupt vector to 0x100

    EXTERN _main
    call _main              ; Jump to C code main()

asm_isr_prt0:               ; PRT0 timer interrupt handler
    di                      ; disable interrupts
    exx                     ; swap to shadow registers
    EXTERN _ISR_prt0
    call _ISR_prt0          ; call the C code handler for this interrupt
    exx                     ; swap the shadow registers back
    ei                      ; enable interrupts again
    reti

First of all, the vector table needs to exist somewhere in memory. On boot, it's set to 0x00, but I changed it to 0x100 in ROM by writing 0x01 to the I register (the high byte of the interrupt base vector). In this vector table are pointers to the various interrupt handlers. In this case only the PRT0 timer has a handler mapped.

This PRT0 timer interrupt has a hardwired interrupt vector of 0x04 (defined in the Z180 manual), so the vector table base plus this offset gives an address of 0x104. In other words, when the PRT0 interrupt fires, the CPU will look up the 16 bit address stored in 0x104 and jump to it, expecting an ISR to exist at that memory location.

That ISR does exist and it's called asm_isr_prt0. This ISR can do pretty much anything, but it should immediately disable further interrupts and re-enable them right before returning. In my case, I've chosen to call a C function to do the bulk of the interrupt handling logic there. It looks like this:

void ISR_prt0()
{
    // Clear the interrupt by reading these registers
    uint8_t a = z180_inp(TCR);
    uint8_t b = z180_inp(TMDR0L);

    asci0_putc('x');
}

Not much to look at, but it's a proof-of-concept for handling interrupts in C. Critically, in the case of the PRT interrupts, TCR and TMDRnL have to be read in order to the clear the interrupt. Without doing this, the interrupt will run in a tight loop forever, blocking out any other code from running. I could not find this anywhere in the Z180 manual, but eventually dug deep enough into the z88dk source that I found something similar.

Finally, the PRT has to be given a counter value and started:

// Load timer 0 with 0x1000 starting value  (roughly 9 ticks per second)
z180_outp(RLDR0H, 0xC0);
z180_outp(RLDR0L, 0x00);

// Enable timer 0 interrupts and start timer 0 counting
z180_outp(TCR, 0b00010001);

I did this in C with the z88dk I/O port wrapper functions.

When interrupts are enabled globally with the EI assembly instruction, Flounder now has an interrupt ticking at about 9 Hz! I don't actually have much of a need for a periodic timer this early in the project, but I've at least proven out the interrupt handling chain.

One final note: I'm pretty sure there's a way in z88dk to directly call a C function as an ISR, but I couldn't make this work. That's why I ended up with the little assembly wrapper around the C call. Optimization for the future...

Discussions