Binary Output on the 6800,
Left-to-right;
Framework-by-include
(asm68c)
We just worked through
three ways to pass parameters at run-time on the 68000
-- and I think it's getting a little tedious to rely on the debugger alone for
seeing what's going on in the program.
Binary output's easy. All you do is look at the bits and spit them out, something like this:
* simple 8-bit binary output for 6800
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
EXP rt_rig6800.asm
****************
* Program code:
*
* Output a 0
OUT0 LDAB #'0
OUT01 JSR PPSHD
JSR OUTC
RTS
*
* Output a 1
OUT1 LDAB #'1
BRA OUT01
* Rob code, shave a couple of bytes, waste a few cycles.
*
* Output the 8-bit binary (base two) number on the stack.
* For consistency, we are passing the byte in the low-order byte
* of a 16-bit word.
OUTB8 LDX PSP ; parameter is at 0,X (low byte at 1,X)
LDAB #8 ; 8 bits
STAB 0,X ; Borrow the upper byte of the parameter.
OUTB8L LSL 1,X ; Get the leftmost bit.
BCS OUTB81
OUTB80 BSR OUT0
BRA OUTB8D
OUTB81 BSR OUT1
OUTB8D DEC 0,X
BNE OUTB8L ; loop if not Zero
INX ; drop parameter bytes
INX
STX PSP
RTS
*
HEADLN FCB CR,LF ; Put message at beginning of line
FCC "Outputting $5A in binary:" ;
FCB CR,LF,NUL ; Put the binary output on a new line
*
*
*
*
PGSTRT LDX #HEADLN
JSR PPSHX
JSR OUTS
CLRA
LDAB #$5A ; byte to output
JSR PPSHD
JSR OUTB8
JSR OUTNWLN
RTS
*
END ENTRY
Well, it's not the most efficient expression of the algorithm, but I think it's reasonable. Separate functions for outputting 0 and 1 seems excessive, but the tail parts are common and we can have one steal code from the other.
Getting the bits is just a matter of shifting them into the carry. Shift left is, by convention of the cultures in which the dominant computer technology developed, shifting the most significant bit out of the register (and thus into Carry). Motorola CPUs (along with most others) do not distinguish between arithmetic and logical shifts going left, and we don't have any reason to care, either.
Why shift left?
When we write, we usually write the most significant first on the left, and then proceed writing to the right. And that's how it proceeds here. You put the most significant bit in Carry by shifting the bits to the left.
Can we do it from right-to-left, instead? Shifting right on bit is essentially dividing by two, isn't it?
Yeah, but then we need a buffer and extra logic, and I'm avoiding that for now.
Finally, where is all the missing code? And what's that EXP pseudo-operator?
EXP is an abbreviation of "expand", and it means to expand (thus, include) the
named file in the place of the line EXP is on. And that file contains all the
missing code, all the useful bits from the preceding chapters.
Here it is:
* A simple run-time framework inclusion for 6800
* providing parameter stack and local base
* Version 00.00.00
* Joel Matthew Rees, October 2024
*
* Essential control codes
LF EQU $0A ; line feed
CR EQU $0D ; carriage return
NUL EQU 0
*
* Essential monitor ROM routines
XOUTCH EQU $F018
*
NATWID EQU 2 ; 2 bytes in the CPU's natural integer
*
*
ORG $80 ; MDOS and EXbug docs say it should be okay here.
ENTRY JMP START
NOP ; Just want even addressed pointers for no reason.
*
* These are the page zero context variables that must be
* saved and restored on process context switch.
* They must never be accessed except in leaf routines:
PSP RMB 2 ; parameter stack pointer
LBP RMB 2 ; local static variable base pointer
XSTKWK RMB 2 ; for stashing X during stack work
XWORK RMB 2 ; for stashing X during other very short operations
*
SSAVE RMB 2 ; a place to keep S so we can return clean
* End of page zero context variables.
*
*
ORG $2000 ; MDOS says this is a good place for usr stuff
LOCBAS EQU * ; here pointer, local static base starts here.
NOENTRY JMP START
NOP
RMB 64 ; room for something
RMB 2 ; a little bumper space
* Not much here
*
SSTKLIM RMB 31 ; 16 levels of call, max
SSTKBAS RMB 1 ; 6800 is post-dec (post-store-decrement) push
RMB 2 ; a little bumper space
PSTKLIM RMB 64 ; 16 levels of call at two parameters per call
PSTKBAS RMB 2 ; bumper space -- parameter stack is pre-dec
*
*
INITRT LDX #PSTKBAS ; Set up the run-time environment
STX PSP
LDX #LOCBAS
STX LBP
TSX ; point to return address
LDX 0,X ; return address in X
INS ; drop the return pointer on stack
INS
STS SSAVE ; Save what the monitor gave us.
LDS #SSTKBAS ; Move to our own stack
JMP 0,X ; return via X
*
*
*********************
* Low-level library:
*
* Only alters X
PPOPX LDX PSP
LDX 0,X
STX XSTKWK
LDX PSP
INX
INX
STX PSP
LDX XSTKWK
RTS
*
* Trashes A,B;
* X points to X value just pushed -- PSP top of stack -- at end
PPSHX STX XSTKWK
LDAA XSTKWK
LDAB XSTKWK+1 ; Falls through
* X points to PSP top of stack at end
PPSHD LDX PSP
DEX
DEX
STX PSP
STAA 0,X
STAB 1,X
RTS
*
*
* X points to PSP top of stack at end
PPOPD LDX PSP
LDAA 0,X
LDAB 1,X
INX
INX
STX PSP
RTS
*
* Load a constant from the instruction stream into A:B,
* continue execution after the constant.
* This is not self-modifying code, even though it feels like a trick
* and is playing with the return stack and instruction stream
* in ways we wouldn't think we wanted to think we should.
* Call it a "necessary" bit of run-time syntactic sugar.
*
* Use it like this:
* JSR LD16I ; load D immediate
* FDB $1234 ; "immediate" 16-bit value to load
* JSR SOMEWHERE ; or some other executable code.
*
LD16I TSX ; point to top of return address stack
LDX 0,X ; point into the instruction stream
LDAA 0,X ; high byte from instruction stream
LDAB 1,X ; low byte from instruction stream
INS ; drop the return address we don't need
INS
JMP 2,X ; return to the byte after the constant.
*
OUTNWLN LDAA #CR ; driver level code to output a new line
BSR OUTCV
LDAA #LF
BSR OUTCV
RTS
*
OUTC JSR PPOPD ; get the character in B
TBA ; put it where XOUTCH wants it.
BSR OUTCV ; output A via monitor ROM
RTS
*
OUTCV JMP XOUTCH ; driver code for outputting a character
*
OUTS JSR PPOPX ; get the string pointer
OUTSL LDAA 0,X ; get the byte out there
BEQ OUTDN ; if NUL, leave
BSR OUTCV ; use the same call OUTC uses.
INX ; point to the next
BRA OUTSL ; next character
OUTDN RTS
*
*
******************************
* intermediate-level library:
*
* input parameters:
* 16-bit left, right
* output parameter:
* 16-bit sum
ADD16 LDX PSP
LDAB 3,X ; left low
LDAA 2,X ; left high
ADDB 1,X ; right low
ADCA 0,X ; right high, with carry
STAB 3,X ; sum low
STAA 2,X ; sum high
INX ; adjust parameter stack
INX
STX PSP
RTS
*
* input parameters:
* 16-bit left, right
* output parameter:
* 16-bit difference
SUB16 LDX PSP
LDAB 3,X ; left low
LDAA 2,X ; left high
SUBB 1,X ; right low
SBCA 0,X ; right high, with borrow
STAB 3,X ; difference low
STAA 2,X ; difference high
INX ; adjust parameter
INX
STX PSP
RTS
*
*
************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START JSR INITRT
*
JSR PGSTRT
*
DONE LDS SSAVE ; restore the monitor stack pointer
NOP ; remember to set a breakpoint here!
NOP ; landing pad
NOP
NOP
LDX $FFFE ; alternatively, get reset vector
JMP 0,X ; and reboot through it
*
* Anyway, if running in EXORsim,
* Ctrl-C should bring you back to EXORsim monitor,
* but not necessarily to your program in a runnable state.
Save it as
rt_rig6800.asm
so that the bit output code can find it and include it.
But, wait. Can EXORsim's interactive assembler do that?
Good question. Pasting it into an assembly session, it just says, "huh" at that line, even if I copy the file into EXORsim's execution director.
It looks like we need to graduate to a separate assembler. And I happen to have one for the 6800 and 6801. It's ancient, but it does the job well enough, and it accepts the EXP pseudo-operator.
I call it asm68c, which can be confusing, because there's another unrelated product out there by someone else with the same name. This is the canonical repository for my assembler:
https://sourceforge.net/projects/asm68c/
Ignore the big green download button and find the link to the Git repository a
bit below and to the right. Click the Git link and then click the
code
link that shows in the pop-up menu. (Ignore a68c-Code. I need to do something about that abortive branch sometime. I promise I will. Just ignore it. Don't click it. It's old code. Sorry.)
You can either download a snapshot (.zip format, but it'll do.) or use git to
clone it. You probably don't want to unpack or clone it in the same directory
that you're working on the tutorial in, however. Find a good out-of-the-way
place to build it.
Scroll down the browser listing of the source code tree and you'll see the README file. I give rough instructions there for building it, but they work. I'll summarize them here. Just go to the top directory after you clone or unpack it and give it the
make
command, if you have a compatible make in your system (which, after building EXORsim, I think you must). That will build and test it.
Or the simple compile command
cc -Wall -o asm68c *.c
should do the job as well. Simply compiling won't attempt to assemble the test files and compare output, but it should be fine.
Move, copy, or link the
asm68c
executable to your local executables directory and make sure the permissions are right and you should be good to go. Or you can invoke by the full path. Either way works.
You should be able to type the
asm68c -?
command at the terminal command line and get a listing of command-line
options, most of which you should ignore, at least for now.
Once it's built and the executable is installed in an appropriate place and running, make sure you've saved the run-time rigging code as
rt_rig6800.asm
and the binary output function and test code in the same directory, as
outb8_6800.asm
Then the command line
asm68c -l2 outb8_6800.asm
should assemble the code, spitting a listing file to your screen on the second pass, and leaving the s1/s9 object code in a file named
outb8_6800.x
which you can open with a text editor. Or you can redirect the assembler listing to a file with
asm68c -l2 outb8_6800.asm > outb8_6800.list
Actually, you do want to redirect the listing to a file, because you'll want to refer to it while stepping through the code and debugging it. But there is something satisfying about watching the assembler listing scroll up the screen ...
Heh. (cough) anywho
If you aren't getting the listing, either you've downloaded the old code or
you haven't given the "-l2" option correctly. That's a hyphen, a little el,
and a 2. It means, "list from the 2nd pass."
Check the listing for errors. If the line after it says pass 2 is done says no syntax errors found, it should be good, but you should use your text editor's search function to make sure that no "error"s or "warning"s show up anywhere.
Fix the errors in the assembly language source file, not the listing file!
(I can't tell you how many times I've found myself editing the listing and
wondering why it doesn't change the compiled code. 8-*)
If there are no errors, you can open the file
outb8_6800.x
select the whole file and copy it, open another terminal session wherever you usually run EXORsim, run the 6800 version with
exor --mon
if it's in your executable path, or if you are running it where you built it (like I do), with
./exor --mon
and get a 6800 session going. At the EXORsim monitor's % prompt, hit the el
key ("l" for "load") and hit return, and it will wait for you to paste the
s1/s9 object code in. You may have to hit return again at the end, to bring it
back to the % prompt:
6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% l
S10700807E213C019C
S10B0084000000000000000070
S105008C00006E
S11320007E213C01000000000000000000000000F0
S113201000000000000000000000000000000000BC
S113202000000000000000000000000000000000AC
S1132030000000000000000000000000000000009C
S109204000000000000096
S11320460000000000000000000000000000000086
S11320560000000000000000000000000000000076
S11320660000000000000000000000000000000066
S11320760000000000000000000000000000000056
S11320860000000000000000000000000000000046
S11320960000000000000000000000000000000036
S10720A60000000032
S11320AACE20A8DF84CE2000DF8630EE0031319FB7
S10920BA8C8E20656E000F
S11220C0DE84EE00DF88DE840808DF84DE8839E2
S10920CFDF889688D68923
S10E20D5DE840909DF84A700E701395D
S10E20E0DE84A600E6010808DF843956
S10E20EB30EE00A600E60131316E0269
S10C20F6860D8D0C860A8D083953
S10A20FFBD20E0178D01393B
S10621067EF0184C
S1102109BD20C0A60027058DF40820F7397D
S1132116DE84E603A602EB01A900E703A70208088A
S1062126DF843916
S1132129DE84E603A602E001A200E703A702080889
S1062139DF843903
S106213CBD20AA15
S106213FBD21912A
S10E21429E8C01010101FEFFFE6E00F7
S10C214DC630BD20D5BD20FF39C8
S1072156C63120F575
S113215ADE84C608E700680125048DE720028DECB9
S10C216A6A0026F20808DF84393A
S11321730D0A4F757470757474696E672024354144
S111218320696E2062696E6172793A0D0A005D
S1132191CE2173BD20CFBD21094FC65ABD20D5BD67
S10921A1215ABD20F639AD
S90321A734
PC set to 21a7
437 bytes loaded
No checksum errors.
%
From there you can unassemble the code, get memory dumps, and so forth, comparing what's in memory to what shows in the listing. When you're satisfied it all got safely put where it's supposed to be, proceed to step through and debug as usual:
% s 80
0 A=00 B=00 X=0000 SP=00FF ------ 0080: 7E 21 3C JMP 213C EA=213C
> 1 A=00 B=00 X=0000 SP=00FF ------ 213C: BD 20 AA JSR 20AA
6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% r
PC=213C A=00 B=00 X=0000 SP=FF CC=C0
% b 2145
Breakpoint set at 2145
% c
Breakpoint!
Outputting $5A in binary:
01011010
951 A=0A B=30 X=20A8 SP=00FF ------ 2144: 01 NOP
> 952 A=0A B=30 X=20A8 SP=00FF ------ 2145: 01 NOP
6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
%
At least, that's what you should be able to get once you have it all set up
correctly.
One of the things I need to fix in my assembler is to provide an option for making EXORsim compatible "facts" files from the symbol table. Until I do, you'll need to look the label addresses up in the assembler listing.
Just for reference, OUT0 and OUT1 can be absorbed into OUTB8, for a slight optimization, as below. (I've hinted at a further optimization in the comments.)
* simple 8-bit binary output for 6800, slightly optimized
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
EXP rt_rig6800.asm
****************
* Program code:
*
* Output the 8-bit binary (base two) number on the stack.
* For consistency, we are passing the byte in the low-order byte
* of a 16-bit word.
OUTB8 LDX PSP ; parameter is at 0,X (low byte at 1,X)
LDAB #8 ; 8 bits
STAB 0,X ; Borrow the upper byte of the parameter.
OUTB8L LSL 1,X ; Get the leftmost bit.
BCS OUTB81
OUTB80 LDAA #'0
BRA OUTB8B
OUTB81 LDAA #'1
OUTB8B JSR OUTCV
DEC 0,X ; B is actually preserved, but this also works.
BNE OUTB8L ; loop if not Zero
INX ; drop parameter bytes
INX
STX PSP
RTS
*
HEADLN FCB CR,LF ; Put message at beginning of line
FCC "Outputting $5A in binary:" ;
FCB CR,LF,NUL ; Put the binary output on a new line
*
*
*
*
PGSTRT LDX #HEADLN
JSR PPSHX
JSR OUTS
CLRA
LDAB #$5A ; byte to output
JSR PPSHD
JSR OUTB8
JSR OUTNWLN
RTS
*
END ENTRY
And that's enough for one chapter. After you've played around a bit more with this, let's do it on the 6801.
Speaking of tedious, I'm running out of time to repeat this tutorial for parameters on the return stack and for static parameters. You've seen enough to do those yourself at this point.
If you are interested, please give it a try.
I'm going to set those aside, except possibly where it comes time to talk about stack frames.
If you do it yourself, you'll learn a lot.
I think it will also convince you how things that look simple often aren't, especially when you don't have automated tools helping you track what's being done where.
I think you'll quickly see how the return address really does get tangled up
in your variables and parameters when you do that, so much so that stack
frames become de rigueur rather quickly. As I say, I may do one more example
where we can see stack frames start turning into a bottleneck. Maybe.
And you'll also see how quickly static parameters tend to devolve into either making lots of little stacks or doing lots of copying to the return address stack, which demonstrates why you really want to make as few variables statically allocated as possible in the first place -- although it does turn out that a few statically allocated variables are necessary in pretty much any real application.
No comments:
Post a Comment