Times, backlights and off-page calls

Sunday, 14th September 2008

Dates, times and backlights

I'm using a DS1307 real-time clock to provide the computer with real-time date and time functions. It's a great little chip - all it needs is power, two lines for I2C communications, a 32768Hz crystal between two pins and a back-up battery to keep it ticking when main power is removed and it's happy. That accounts for seven pins; the last remaining pin can be used as a one-bit output (you can set it to a high or low state in software) or it can be configured to output a square wave at 1Hz, ~4kHz, ~8kHz or ~32kHz.

2008.09.14.01.Clock.png

BBC BASIC can access the clock via the TIME$ pseudo-variable. This string variable returns the date and time in the format Sun,14 Sep 2008.15:20:00, and you can set the clock by assigning to the variable. When setting the clock you can specify either the date, the time, or both. Parsing the string has been an interesting exercise in Z80 programming, as it's not something I've ever attempted without regular expressions before!

2008.09.14.02.SettingClock.jpg

The only hardware modification since last time is a very poorly implemented software control of the backlight. The fifth bit of the control port specifies whether the backlight is on or off, and it can be toggled with the *BACKLIGHT command. I say "poorly implemented" as the transistor driver I'm using to interface the hardware port with the backlight LEDs results in a much dimmer backlight than when I had the LEDs hooked up directly to the power supply (on the positive side, at least the 5V regulator's heatsink is cool enough to touch - the backlight draws a lot of current).

Calling off-page functions

Now that I have access to all eight 16KB "pages" that make up the 128KB OS ROM, it may help to explain how one can use all of this memory. After all, if page 1 is swapped in and you wish to call a function on page 2, a regular Z80 call isn't going to work as you need to swap page 2 before calling the function then swap page 1 back in afterwards.

The trick is to exploit the way that the Z80 handles calling subroutines. There is a 16-bit register, PC, which stores the address of the next instruction to execute. When you call a subroutine, the Z80 pushes PC onto the stack then sets PC to the address of the subroutine. When you return from a subroutine (via the ret instruction) the Z80 simply pops the value it previously pushed onto the stack and copies this back to PC. Instead of calling the target subroutine directly, you call a special handler that is available on every page. Following your call is 16-bit identifier for the off-page function you wish to call. This handler then (prematurely) pops off the return address from the stack, reads the 16-bit value that follows it (which is the indentifier of the function you wish to call), looks up the page and address of the target function, swaps in the correct page and calls it as normal. When the function returns, the handler then swaps back the calling page and jumps back to the return address.

The Z80 has a series of rst instructions that call fixed addresses within the first 256 bytes of memory. These instructions are useful as they're small (one byte vs three bytes for a regular call) and fast, so I'm using rst $28 to call the off-page call handler (for no other reason than it's the same as the handler on the TI-83+).

As an example, let's say you had this function call at address $2B00:

$2B00:    rst $28
$2B01:    .dw $30F0
$2B03:    ; We'd return here.

When the Z80 executed that rst $28 it would push $2B01 (address of the next instruction) to the stack then jump to $28. The handler at $28 would do something like this:

    pop hl    ; hl is a 16-bit register and would now contain $2B01
    ld e,(hl) ; Read "e" from address pointed to by hl, now equals $F0
    inc hl    ; hl = $2B02
    ld d,(hl) ; Read "d" from address pointed to by hl, now equals $30
    inc hl    ; hl = $2B03 ("real" return address)
    push hl   ; push hl back on the stack so when we return from here we end up in the correct place.

Now, de is $30F0 - this is the identifier of the function we're calling. In my case, the identifier points to a function table on page 0. Each entry in the table is three bytes - one byte for the page index and two bytes for the address of the function on the that page. We'd need to do something like this:

    in a,(Page)  ; Read the current page into A.
    push af      ; Push A and F to the stack for later retrieval.
    and ~7       ; Mask out the lower three bits of the address.
    out (Page),a ; Sets current ROM page to 0.
    ex de,hl     ; Exchanges de and hl, so hl now points to the function identifier.
    or (hl)      ; ORs contents of memory at (hl) (ie, page number) with a, to set the target page.
    inc hl
    ld e,(hl)    ; e = LSB of target address
    inc hl
    ld d,(hl)    ; d = MSB of target address
    ex de,hl     ; hl = target address.
    out (Page),a ; Swaps in the correct page.
At this point, the correct page is swapped in and hl points to the address of the function to call. All we need to do now is call it!
    ld de,ReturnFromHandler ; Address to return to.
    push de ; Store on stack.
    jp (hl) ; Set pc = hl.
ReturnFromHandler
    ; Swap back the original page which was pushed earlier...
    pop af
    out (Page),a
    ret ; ...and return to the calling page!

A further advantage of using rst $28 to replace call is that both are the same size, so the assembler can check if you're calling an address on the same page or a different one and insert the regular (and much faster) Z80 call in places where you don't need to swap the page.

Finally, the obligatory video, this time showing a clock that toggles the backlight once a second.

FirstPreviousNextLast RSSSearchBrowse by dateIndexTags