Sunday, August 11, 2024

ALPP 01-11 -- Hello, World! -- (Not Yet on the Beach) -- 68000

Hello, World!
(Not Yet on the Beach)
68000

(Title Page/Index)

 

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.

Again, our target for this exercise is to get a string of characters, such as "On the beach!" or "Hello World!" output on the screen where we can read it. 

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.


(Title Page/Index)

 

 

 

 

No comments:

Post a Comment