Foothold!
(Split Stacks ... barely on the Beach)
68000
In the last chapter, we
took a bit of a closer look at how to use many of the 6809's key
improvements
over the 6800 and 6801. In this chapter, we'll look at using similar features
in the 68000.
Again, you may want to compare the code in separate browser windows or even on separate monitors.
Where the 6809 gives us either two 8-bit accumulators or one 16-bit accumulator, the 68000 gives us a monstrous eight -- D0 ~ D7. Where the 6809 gives us four or five generally indexable registers -- X, Y, U, S, and PC, the 68000 gives us eight or ten -- A0 ~ A6, system and user A7, and PC.
Anyway, let's take a look at how OUTC can change on the 68000. Somewhat randomly, I am choosing to use A6 as the the parameter stack pointer. OUTC can use the 68000's pop equivalent instructions:
* In these examples, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC MOVEM.L (A6)+,D7 ; Get the character, all 32 bits
AND.W #$00FF,D7 ; clear character high byte.
MOVEM.W D7,-(A7) ; Put it where bconout wants it, on the A7 stack.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
Note that this looks like a lot more code than the 6809 code, but most of that is for the BIOS call. Where the call into the monitor ROM on the 6809 was one instruction, the BIOS call here is four, including the final deallocation. Also, in the 6809 monitor code, we did not need to clear the high byte, and here we do.
So it would be essentially the same number of instructions if we were
comparing like calls with like.
We could clear the high byte after we move it where bconout expects it:
OUTC MOVEM.L (A6)+,D7 ; Get the character, all 32 bits.
MOVEM.W D7,-(A7) ; Put 16 bits of it where bconout wants it.
CLR.B (A7) ; Clear the high byte on the A7 stack.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
Or we could clear D7 before we get just the byte:
* We can handle the parameter stack directly.
OUTC CLR.W D7 ; clear the high byte for the character.
MOVE.B NATWID-1(A6),D7 ; Get the character.
ADDQ.L #NATWID,A6 ; drop it from parameter stack.
MOVEM.W D7,-(A7) ; Put it where bconout wants it, on the A7 stack.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
What if we even want to preserve D7? (We probably don't, but ...):
OUTC LEA NATWID/2(A6),A6 ; get the high 16 bits out of the way
MOVE.W (A6)+,-(A7) ; move the character from one stack to the other
CLR.B (A7) ; clear character high byte, as bconout wants it.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
When you want to adjust an address or other value by small amounts, the ADDQ instruction is usually the Quickest. Think of it as INC by anything from 1 to 8.
Load Effective Address using constant offset also works, similar to the 6809.
Again, we have PC relative source addresses to help keep the code easy to move from place to place. Here, too, the assembler figures out the offset from the label, so you use the label in the source code, but the offset is assembled to the object code.
Again, if you haven't already, open up separate browser windows for the introduction to split stacks on the 6809 so you can compare.
Of course, we don't need any equivalent to the PPUSHD and PPOPD routines. I've used the MOVEM instruction in the above examples, to let you know MOVEM is there. Also, MOVEM does not alter the CPU processor state flags, like a straight MOVE would. But a straight MOVE with auto-increment or -decrement can be used if you only have one register to push or pop.
MOVEM, like the 6809 PSH and PUL, can move Multiple registers with a single instruction.
One word of warning about using MOVEM as the default POP instruction --
Even though it doesn't affect the flags, it does sign-extend 16-bit values
loaded into data registers. So you'll need to keep that in mind.
The 68000 routine to set up the stack pointers looks a lot like the 6809
routine::
INISTKS LEA PSTKBAS(PC),A6 ; Set up the parameter stack
MOVEM.L (A7),A0 ; 68000 lets us do this -- return address in A0
MOVE.L A7,D7 ; Save what the monitor gave us.
LEA SSTKBAS(PC),A7 ; Move to our own stack
MOVEM.L D7,-(A7) ; Save monitor's stack pointer on ours
JMP (A0) ; return via A0
When we're done, we can restore the monitor's stack pointer with a simple
load:
DONE MOVEM.L (A7),A7 ; restore the BIOS stack pointer
Let's look at the complete code to test character output:
OPT LIST,SYMTAB ; Options we want for the stand-alone assembler.
MACHINE MC68000 ; because there are a lot the assembler can do.
OPT DEBUG ; We want labels for debugging.
OUTPUT
***********************************************************************
*
NATWID EQU 4 ; 4 bytes in the CPU's natural integer
BIOSTRAP EQU 13
bconout EQU 3
devscrkbd EQU 2
EVEN
ENTRY JMP START
EVEN
* No need for PSP in 68000 --
* A6 will be our parameter stack pointer
*
* No need for SSAVE, either. We will keep SSAVE in an unused register.
*
SSTKLIM DS.L 16 ; 16 levels of call, max
* 68000 is pre-dec (decrement before store) push
SSTKBAS DS.L 1 ; a little bumper space
PSTKLIM DS.L 16*2 ; 16 levels of call at two parameters per call
PSTKBAS DS.L 1 ; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.)
* MOVEM is the designated PUSH/POP for the 68000, sort-of.
* Usually it is used to save and restore multiple registers in one instruction.
* It can also be used to avoid messing with the processor state flags.
* We're using it to bring it to yor attention.
* Otherwise, for only one register, you will usually use a normal MOVE.
* Unless you want to preserve flags.
INISTKS LEA PSTKBAS(PC),A6 ; Set up the parameter stack
MOVEM.L (A7),A0 ; 68000 lets us do this -- return address in A0
MOVE.L A7,D7 ; Save what the monitor gave us.
LEA SSTKBAS(PC),A7 ; Move to our own stack
MOVEM.L D7,-(A7) ; Save monitor's stack pointer on ours
JMP (A0) ; return via A0
* No need for PPOPD or PPUSHD in 68000 code.
* We can handle the parameter stack directly.
* In this example, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC CLR.W D7 ; clear the high byte for the character.
MOVE.B NATWID-1(A6),D7 ; Get the character.
ADDQ.L #NATWID,A6 ; drop it from parameter stack.
MOVEM.W D7,-(A7) ; Put it where bconout wants it, on the A7 stack.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
*
START BSR.W INISTKS ; 68000 lets us do medium range relative branches
*
MOVE.L #'H',-(A6) ; push character to be output at natural width
BSR.W OUTC
*
DONE MOVEM.L (A7),A7 ; restore the BIOS stack pointer
NOP
NOP
* One way to return to the OS or other calling program
clr.w -(sp) ; there should be enough room on the caller's stack
trap #1 ; quick exit
And that's it.
Here again, line buffering requires you to (c)ontinue the code through the
return to the TOS command line, to see the character output.
Save the code as something like "out_H_2s.s" in the directory where you're
doing the 68000 Atari ST work and use vasm to assemble it, using a
command something like
$ vasmm68k_mot -Ftos -no-opt -o OUT_H_2s.PRG -L out_H_2s.lst out_H_2s.s
Refer back to Hello World for 68000 example in the H68000's Hello World chapter if you don't remember how to invoke the debugger, set breakpoints, and run it.
Unlike the 6800/6801 and 6809, the 68000 has an exception to break on
undefined op-codes. But you don't want to depend on garbage in memory being
undefined op-codes, so do set breakpoints.
* First 68000 Foothold on the split stack beach,
* by Joel Matthew Rees September 2024, Copyright 2024 -- All rights reserved.
*
OPT LIST,SYMTAB ; Options we want for the stand-alone assembler.
MACHINE MC68000 ; because there are a lot the assembler can do.
OPT DEBUG ; We want labels for debugging.
OUTPUT
***********************************************************************
* We will not be using these:
*GEMDOSTRAP EQU 1
*GEMprintstr EQU 9 ; PRINT LINE in some docs
BIOSTRAP EQU 13
bconout EQU 3
devscrkbd EQU 2
LF EQU $0A ; line feed
CR EQU $0D ; carriage return
NUL EQU 0
* Opinions may vary about the natural integer.
NATWID EQU 4 ; 4 bytes in the CPU's natural integer
EVEN
ENTRY BRA.W START
* No need for PSP in 68000 --
* A6 will be our parameter stack pointer
*
* No need for SSAVE, either. We will keep SSAVE in an unused register.
*
SSTKLIM DS.L 16 ; 16 levels of call, max
* 68000 is pre-dec (decrement before store) push
SSTKBAS DS.L 1 ; a little bumper space
PSTKLIM DS.L 16*2 ; 16 levels of call at two parameters per call
PSTKBAS DS.L 1 ; bumper space -- parameter stack is pre-dec
* (But this example doesn't use half of that number of levels.)
*
HELLO DC.B CR,LF ; Put message at beginning of line
DC.B "SEKAI YO, YAI!" ; Whatever the user wants here.
DC.B CR,LF,NUL ; Put the debugger's output on a new line.
* MOVEM is the designated PUSH/POP for the 68000, sort-of.
* Usually it is used to save and restore multiple registers in one instruction.
* It can also be used to avoid messing with the processor state flags.
* We're using it to bring it to yor attention.
* Otherwise, for only one register, you will usually use a normal MOVE.
* Unless you want to preserve flags.
EVEN
INISTKS LEA PSTKBAS(PC),A6 ; Set up the parameter stack
MOVEM.L (A7),A0 ; 68000 lets us do this -- return address in A0
MOVE.L A7,D7 ; Save what the monitor gave us.
LEA SSTKBAS(PC),A7 ; Move to our own stack
MOVEM.L D7,-(A7) ; Save monitor's stack pointer on ours
JMP (A0) ; return via A0
* No need for PPOPD or PPUSHD in 68000 code.
* We can handle the parameter stack directly.
* In this example, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC CLR.W D7 ; clear character high byte.
MOVE.B NATWID-1(A6),D7 ; Get the character, high byte cleared
ADDQ.L #NATWID,A6 ; drop it from parameter stack.
BSR.S OUTCV ; Can't just fall through if we're going to share.
RTS
*
* Return from TRAP is RTE,not RTS,
* therefore, this must be called as a subroutine, not just jumped to.
* Common hook: call with the character on A7 stack.
* This bit of glue calls the BIOS.
OUTCV MOVE.W D7,-(A7) ; Push the character on A7 where bconout wants it.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
*
* And we can handle the parameter stack directly here, too.
* A3 is preserved by Hatari BIOS.
OUTS MOVEM.L (A6)+,A3 ; get the string pointer
OUTSL CLR.W D7 ; Prepare the high byte.
MOVE.B (A3)+,D7 ; get the byte, update the pointer
BEQ.S OUTDN ; if NUL, leave before pushing the character.
BSR.S OUTCV ; use the same call OUTC uses.
BRA.S OUTSL ; next character
OUTDN RTS
*
* All the newlines we need are in the string.
*
START BSR.W INISTKS ; 68000 lets us do medium range relative branches
*
LEA HELLO(PC),A0 ; push address of string to be output
MOVEM.L A0,-(A6) ; A0 is not preserved by Hatari BIOS, by the way.
BSR.W OUTS ; put it on the screen,
DONE MOVE.L (A7),A7 ; restore previous user stack pointer
NOP
NOP
* One way to return to the OS or other calling program
clr.w -(sp) ; there should be enough room on the caller's stack
trap #1 ; quick exit
Note again, all the uses of the auto-increment index mode, not just in the string output.
Note also the choices of index registers.
In particular, I got trapped in forgetting that A0 is volatile across BIOS
calls and had to debug some code before putting it up here. Can't use it and
expect it to be there after a BIOS call, but we can use it for scratch
calculations and temporarily holding addresses when we don't call a subroutine
between setting it and using it. (This is information in the Atari
manuals.)
Also note that we don't put a few BIOS parameters on the A7 stack and then
jump into a subroutine that puts more on the A7 stack, because that would
require moving return addresses out of the way. I got bitten by this, trying
to push the character on A7 before calling OUTCV and pushing the BIOS call
selectors on A7. (Remember what I said about using just one stack?)
And the 68000 also makes it is quite easy to have OUTS call OUTC rather than the common code subroutine, like this:
OUTS MOVEM.L (A6)+,A3 ; get the string pointer
OUTSL CLR.L D7 ; Prepare the high byte.
MOVE.B (A3)+,D7 ; get the byte, update the pointer
BEQ.S OUTDN ; if NUL, leave before pushing the character.
MOVE.L D7,-(A6) ; push it on the parameter stack.
BSR.S OUTC ; use OUTC itself.
BRA.S OUTSL ; next character
OUTDN RTS
Here we can get a peek at one of the differences between the 6809 and the 68000.
Concatenating low bytes and high bytes is easy on the 6809. Not so easy on the 68000 -- so much so that when you need to clear the higher part of an integer on the 68000, it's generally best to clear a free register before loading the lower part of the integer.
Like the 6809, it takes just a little extra code to call OUTC rather than the common subroutine, but, like the 6809, it can make it easier to manage character output related code, and, like the 6809, separating the common code into a shorter subroutine may be useful.
Which way do I recommend? It depends, and I'll tell you more about that as we go.
Again, lightly tested, should work for you. Play with the code a little while
we're here. And think about checking whether the stacks are in balance.
After you've played with it a bit, let's start looking at doing integer arithmetic at byte level on the three 8-bit processors.
No comments:
Post a Comment