Hello, World!
(Not Yet on the Beach)
68000
Now that we have waded through
getting text on the terminal screen with the 6800/6801 and 6809
under the EXORbug monitor ROM EXbug, let's take a look at how to do it on the
68000 under the Atari/EmuTOS BIOS -- and DOS.
The larger address space of the 68000 allows working efficiently with larger character sets, but even the older Japanese Industrial Standards JIS codes (roughly 2000 characters at the time, now much more), containing just Japanese Kanji and Kana, US ASCII, Greek, and Cyrillic, were enough to bring a Macintosh with an early 68000 to its knees unless the application software engineers really knew what they were doing. (Parsing shift-JIS could become especially intransigent in certain situations)
As far as I know, Atari never officially tried to deal with character sets as large as the JIS encodings for the ST, much less complete Unicode.
But it has fairly complete US-ASCII plus a bit, and that will work fine for us
for now.
With the EXORciser, we used the EXbug monitor routines for outputting characters and strings.
Character Output
With the Atari ST, we'll use the BIOS routines for outputting characters.
The BIOS routines are documented in several sources. The one which I have seen most recommended so far is Abacus Software's Atari ST Internals, by Gerits, Englisch, and Bruckmann. You can find PDFs of it which can be downloaded, but I am not in a position to tell you whether Abacus Software or the authors or their successors in interest have any particular opinions about you downloading it.
I'll try to provide enough of the documentation of the routines we use for our purposes here.
Facts about BIOS calls on the Atari ST to (partially) absorb before we dive in --
- BIOS routines are called via the 68000's TRAP instruction, which is similar to but different from the 8086's INT instruction, and kind of similar to but more different from the 6800/6801/6809 SWI instruction.
- Specifically, TRAP 13 is used for BIOS calls on the Atari ST.
- Doing BIOS calls via TRAP instructions takes more time than a direct call (JSR or BSR) into a monitor ROM would, significantly more than twice as long.
- But the TRAP call also provides the means of switching from USER state to SUPERVISOR state, supporting putting a wall between system data and user code, and such.
-
By Atari ST BIOS convention, call parameters for the BIOS routines are
pushed on the A7 stack before the TRAP is issued. (Compare this to the EXbug
convention of using registers and a simple call.)
- The BIOS routine number will also be pushed onto the A7 stack, as the last parameter pushed before the TRAP is issued.
- Registers D0-D2 and A0-A2 are volatile across the BIOS calls. Save them if you need them.
- Register D0 will carry the return value on return.
The routine we will use to output a character to the screen is called bconout, BIOS routine number 3. It takes two parameters, the character to output and the device to output it to, in that order.
Device numbers are
- 0: (PRT) printer port (Centronics)
- 1: (AUX) serial port (RS-232C)
- 2: (CON) console (keyboard/screen)
- 3: (MIDI) MIDI interface
- 4: (IKBD) programmable keyboard interface
This snippet of code demonstrates how the above could be used to output the
character 'H' to the screen:
MOVE.W #'H',-(A7) ; push character to be output
MOVE.W #2,-(A7) ; push the device number for the screen
MOVE.W #3,-(A7) ; push the bconout BIOS routine selector
TRAP #13 ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the parameters when done
Explicit pushes on the 68000 are accomplished by predecrement MOVE instructions
targeting the stack register. The order in which the arguments are pushed
matters.If, for example, at the point of the TRAP instruction,
- user A7 has the address $00077FF2
- superviser A7 has the address $00007E64
- PC has the address $00013d22, and
- SR/CC has only the interrupt mask set to level 3 ($0300)
here's what the BIOS code will see on the user and system stacks at the start of the TRAP 13 service routine:
Address | Data | Description |
---|---|---|
User Stack: | ||
$00077FF8: | (arbitrary) | |
$00077FF6: | $0048 | 'H' (ASCII) |
$00077FF4: | $0002 | Device # |
$00077FF2: | $0003 | bconout code |
System Stack: | ||
$00007E62: | $3d24 | Return Address low word |
$00007E60: | $0001 | Return Address high word |
$00007E5E: | $0300 | SR/CC state before TRAP |
From there, the BIOS code will proceed to figure out that the call is to put
the H on the screen at the current cursor position, etc., and do it.
If you're thinking that's a lot more work than we were doing with EXbug, and a lot more than EXbug was doing, you're right. If you're wondering whether it's reasonable, well, ... it's a long story. We need some background. Anyway, that's what it looks like.
Here's some code to try out on Hatari:
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
***********************************************************************
*
BIOSTRAP EQU 13
bconout EQU 3
devscrkbd EQU 2
EVEN
ENTRY JMP START
*
START MOVE.W #'H',-(A7) ; push character to be output
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 parameters when done
NOP
DONE 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
You could save that as something like "out_H.s", and then assemble it with
something like
vasmm68k_mot -Ftos -no-opt -o OUT_H.PRG -L out_H.lst out_H.s
You might want to make a new directory to keep this work separate from the earlier work, and save the file there before you assemble it.
Remember, after you start hatari, to
- use Ctrl-C to break out of the GUI shell into the command-line shell,
- change to your new working directory,
- use Alt-PAUSE to break into the debugger,
- set a breakpoint on entry to TEXT,
- continue back to the command-line shell,
- run the program by the program file name you gave to vasm68k,
-
and show the registers before and while you step through the code.
Both before you execute the TRAP and after, you can use the memory dump command to look at the stacks, to help see what's going on.
Using the register display command, you will see the user stack pointer (USP)
and system (interrupt) stack pointer (ISP) and you can use those addresses for
the memory dump:
Skipping duplicate address & symbol name checks when autoload is enabled.
Loaded 6 symbols (4 TEXT) from '/home/nova/usr/share/hatari/C:/primer/s2_str/OUT_H.PRG'.
CPU=$13d10, VBL=7282, FrameCycles=62520, HBL=61, LineCycles=544, DSP=N/A
00013d10 4ef9 0001 3d16 jmp $00013d16
> s
CPU=$13d16, VBL=7282, FrameCycles=62532, HBL=61, LineCycles=556, DSP=N/A
00013d16 3f3c 0048 move.w #$0048,-(a7) [0000]
> s
CPU=$13d1a, VBL=7282, FrameCycles=62544, HBL=61, LineCycles=568, DSP=N/A
00013d1a 3f3c 0002 move.w #$0002,-(a7) [0000]
> s
CPU=$13d1e, VBL=7282, FrameCycles=62556, HBL=61, LineCycles=580, DSP=N/A
00013d1e 3f3c 0003 move.w #$0003,-(a7) [0000]
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00000000
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013D2E A5 00013D2E A6 00077FC6 A7 00077FF4
USP 00077FF4 ISP 00007E64
T=00 S=0 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 3f3c (MOVE) 0003 (OR) Chip latch 00000000
00013d1e 3f3c 0003 move.w #$0003,-(a7) [0000]
Next PC: 00013d22
> s
CPU=$13d22, VBL=7282, FrameCycles=62568, HBL=61, LineCycles=592, DSP=N/A
00013d22 4e4d trap #$0d
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00000000
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013D2E A5 00013D2E A6 00077FC6 A7 00077FF2
USP 00077FF2 ISP 00007E64
T=00 S=0 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 4e4d (TRAP) 5c8f (ADDA) Chip latch 00000000
00013d22 4e4d trap #$0d
Next PC: 00013d24
> m $00077FF2 32
00077FF2: 00 03 00 02 00 48 00 00 00 00 00 01 3c 10 c6 00 .....H......<...
00078002: c6 00 38 00 38 00 00 00 00 00 00 00 00 00 00 00 ..8.8...........
> m $0007E64 16
00007E64: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> s
CPU=$e00c4a, VBL=7282, FrameCycles=62604, HBL=61, LineCycles=628, DSP=N/A
00e00c4a 3239 00e0 198c move.w $00e0198c [000c],d1
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00000000
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013D2E A5 00013D2E A6 00077FC6 A7 00007E5E
USP 00077FF2 ISP 00007E5E
T=00 S=1 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 3239 (MOVE) 00e0 (ILLEGAL) Chip latch 00000000
00e00c4a 3239 00e0 198c move.w $00e0198c [000c],d1
Next PC: 00e00c50
> m $77FF2 16
00077FF2: 00 03 00 02 00 48 00 00 00 00 00 01 3c 10 c6 00 .....H......<...
> m $7E5E 16
00007E5E: 03 00 00 01 3d 24 00 00 00 00 00 00 00 00 00 00 ....=$..........
>
You can see the parameters and the BIOS call selector there on the USP stack.
After the call, you can see the status register and return address there on
the ISP stack.
While we are here, look back at the supervisor (S) bit in the status flags and
at the address in A7 to see which stack is active when.
You may not want to trace all the way through the BIOS code. It does get a bit monotonous. Or you might find it interesting, after all. Whatever. Either way, go ahead and step through and/or continue to the end.
Check the screen after you let it run to completion. Look for the 'H' on the next line before the command-line shell prompt, something like
C:\primer\s2_str>OUT_H.PRG
HC:\primer\s2_str>
(If you don't let it run to completion and return to the TOS command-line prompt, you'll find that line buffering won't show you the character you output until it shows you the whole next line with the prompt. Ignore that for now, since it does ultimately show up.)
String Output
There doesn't seem to be a routine in the BIOS to output a string -- nor even
in the extended XBIOS. But there is one in the DOS level calls, called PRINT
LINE in some documentation, which prints an entire string to the screen at the
current cursor position. Again, we won't be using the system library string
output routines for much after this, but let's take a look at how it works.
Things to consider about this call:
- The GEMDOS calls are called by TRAP 1.
- The PRINT LINE function is function number 9.
- The parameter is the 32-bit address of the string.
- The string is terminated by an ASCII NUL (0)
Before looking at the example code below, refer to the example above and see if you can construct an appropriate example on your own. Remember you need to push the parameters and the function code in the correct order, and then make the right TRAP call.
If you define your own stack area, you'll want to know that the 68000's
standard equivalent of RMB is DS.B. Since the assembler also provides DS.L,
you don't have to multiply the number of levels of call you want to provide
room for by the byte width of return addresses.
What do you think? Can you do it?
Compare your work with mine:
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
***********************************************************************
*
GEMDOSTRAP EQU 1
GEMprintstr EQU 9 ; PRINT LINE in some docs
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 JMP START
STKLIM DS.L 32 ; 32 levels minus paramaters and locals
STKBAS EQU * ; 68000 is pre-dec (pre-store-decrement) push
*
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.
*
EVEN
START MOVE.L A7,D7 ; D7 is supposed to be preserved.
MOVE.L #STKBAS,A7 ; load our own stack
MOVE.L #HELLO,-(A7) ; push address of string to be output
MOVE.W #GEMprintstr,-(A7) ; push the BIOS routine selector
TRAP #GEMDOSTRAP ; call into GEMDOS
MOVE.L D7,A7 ; restore previous user stack pointer
* ADDQ.L #6,A7 ; no need to deallocate after restore
NOP
DONE 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
If you save the above as "out_str.s", you can assemble it with
vasmm68k_mot -Ftos -no-opt -o OUT_STR.PRG -L out_str.lst out_str.s
and you can run it under the debugger as you did with the character output
routine. Go ahead and do that and watch the stack as you do:
Loaded 11 symbols (5 TEXT) from '/home/nova/usr/share/hatari/C:/primer/s2_str/OUT_STR.PRG'.
CPU=$13d10, VBL=6908, FrameCycles=97032, HBL=95, LineCycles=512, DSP=N/A
00013d10 4ef9 0001 3daa jmp $00013daa
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00000000
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013DC8 A5 00013DC8 A6 00077FC6 A7 00077FF8
USP 00077FF8 ISP 00007E64
T=00 S=0 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 4ef9 (JMP) 0001 (OR) Chip latch 00000000
00013d10 4ef9 0001 3daa jmp $00013daa
Next PC: 00013d16
> s
CPU=$13daa, VBL=6908, FrameCycles=97044, HBL=95, LineCycles=524, DSP=N/A
00013daa 2e0f move.l a7,d7
> s
CPU=$13dac, VBL=6908, FrameCycles=97048, HBL=95, LineCycles=528, DSP=N/A
00013dac 2e7c 0001 3d96 movea.l #$00013d96,a7
> s
CPU=$13db2, VBL=6908, FrameCycles=97060, HBL=95, LineCycles=540, DSP=N/A
00013db2 2f3c 0001 3d96 move.l #$00013d96,-(a7) [00000000]
> s
CPU=$13db8, VBL=6908, FrameCycles=97080, HBL=95, LineCycles=560, DSP=N/A
00013db8 3f3c 0009 move.w #$0009,-(a7) [0000]
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00077FF8
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013DC8 A5 00013DC8 A6 00077FC6 A7 00013D92
USP 00013D92 ISP 00007E64
T=00 S=0 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 3f3c (MOVE) 0009 (ILLEGAL) Chip latch 00000000
00013db8 3f3c 0009 move.w #$0009,-(a7) [0000]
Next PC: 00013dbc
> s
CPU=$13dbc, VBL=6908, FrameCycles=97092, HBL=95, LineCycles=572, DSP=N/A
00013dbc 4e41 trap #$01
> m $13d92 16
00013D92: 00 01 3d 96 0d 0a 53 45 4b 41 49 20 59 4f 2c 20 ..=...SEKAI YO,
> m $7e64 16
00007E64: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00077FF8
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013DC8 A5 00013DC8 A6 00077FC6 A7 00013D90
USP 00013D90 ISP 00007E64
T=00 S=0 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 4e41 (TRAP) 2e47 (MOVEA) Chip latch 00000000
00013dbc 4e41 trap #$01
Next PC: 00013dbe
> s
CPU=$fa002a, VBL=6908, FrameCycles=97128, HBL=95, LineCycles=608, DSP=N/A
00fa002a 0008 illegal
> r
D0 00000000 D1 00000000 D2 00000000 D3 00000000
D4 00000000 D5 00000000 D6 00000000 D7 00077FF8
A0 00000000 A1 00000000 A2 00000000 A3 00000000
A4 00013DC8 A5 00013DC8 A6 00077FC6 A7 00007E5E
USP 00013D90 ISP 00007E5E
T=00 S=1 M=0 X=0 N=0 Z=0 V=0 C=0 IMASK=3 STP=0
Prefetch 0008 (ILLEGAL) 690a (Bcc) Chip latch 00000000
00fa002a 0008 illegal
Next PC: 00fa002c
> m $13d90 16
00013D90: 00 09 00 01 3d 96 0d 0a 53 45 4b 41 49 20 59 4f ....=...SEKAI YO
> m $7e5e 16
00007E5E: 03 00 00 01 3d be 00 00 00 00 00 00 00 00 00 00 ....=...........
>
Go ahead and step through or continue, and make sure you can get the string of your choice output to the screen:
C:\primer\s2_str>OUT_STR.PRG
SEKAI YO, YAI!
C:\primer\s2_str>
I want to show you the PEA instruction, and I may not have another excuse for
it, so let's look at that again using some of those other addressing modes the
68000 enables. Here's the code, I've stepped through it and it works:
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
***********************************************************************
*
GEMDOSTRAP EQU 1
GEMprintstr EQU 9 ; PRINT LINE in some docs
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
STKLIM DS.L 32 ; 32 call levels minus paramaters and locals
STKBAS EQU * ; 68000 is pre-dec (pre-store-decrement) push
*
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.
*
EVEN
START MOVE.L A7,D7 ; D7 is supposed to be preserved.
LEA STKBAS(PC),A7 ; load our own stack
PEA HELLO(PC) ; push address of string to be output
MOVE.W #GEMprintstr,-(A7) ; push the BIOS routine selector
TRAP #GEMDOSTRAP ; call into GEMDOS
MOVE.L D7,A7 ; restore previous user stack pointer
NOP
DONE 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
Go ahead and assemble it and run it through the debugger. After you step through the PEA instruction, take a look at the stack to make sure the right address is there. Use the (m)emory dump instruction to show appropriate pieces of memory, to make sure you believe it.
Now, the Push Effective Address (PEA) instruction is not going to be nearly as useful to us as we might wish. It only pushes effective addresses on the A7 stack, and we are not going to be doing that very much. But you need to know it exists.
So, we have a sort-of Hello-world! program that isn't really as functional as we wish for a number of reasons.
If you haven't yet gone through the soft introduction to branching and loops that I used to work around issues in my EXORsim6801 branch of EXORsim, you should.
And you might also want to take a detour (I strongly recommend it!) to practice debugging on the 6800 and 6809.
If you have already gone through that, I think we want to walk through using the character output functions to write our own string output functions. And I guess we'll want to understand why, first, or at least as we go.
No comments:
Post a Comment