Amiga Machine Code Letter X - CLI
This is the third part, of a multi-part series about the system libraries. The previous post was about using the DOS library to read and write files.
We are still looking at Letter X of the Amiga Machine Code course, and as always, make sure to read the letter, since I won’t go through all the details.
In this post, and the next one, we will take at how to use the DOS library to interact with the command line interface of the AmigaOS.
CLI Arguments and Output
The Amiga ships with a command line interface, called CLI. In this section we’ll take a look at how I/O from, and to, the CLI works.
Letter X starts by explaining only the most just about enough about the CLI, to get you going with the first example program mc1004.
When the CLI runs a program, it allocates a stack for that program, and puts the pointer to the argument line into register a0. The pointer to the argument line is on the CLI stack and remains valid during program execution. The CLI also also puts the length of the argument line into d0. Note that the last character is typically a return, which we will ignore in the mc1004 program.
The mc1004 program will take an argument from the command line and echo it back. To do this, we need a file handle to the output. The Dos library function Output by will return a file handle, that typically refers to the CLI terminal, unless the output was redirected on the command line, by “>” and “<” like we know from unix.
Output
Description: finds the program´s initial output filehandle
Library: dos.library
Offset: -$3C (-60)
Syntax: fh = Output()
ML: d0 = Output()
Arguments: none
Result: fh = program´s output filehandle
The Output function is also described in the autodocs, where we are told to never close the filehandle. The filehandle was opened for us by the CLI, and should also be closed by the CLI.
Let’s take a look at mc1004, which also can be found on disk1. The program will read an argument from the command line and echo it back into the same command line.
cmp.w #1,d0 ; compare 1 with d0 (argument length from CLI)
ble noarg ; if d0 <= 1, then goto noarg
lea.l argbuffer,a1 ; move argbuffer address into a1
move.l d0,d7 ; move d0 into d7
copyarg: ; copy argument
move.b (a0)+,(a1)+ ; move value pointed to by a0 into argbuffer and post increment both
subq.w #1,d0 ; subtract 1 from argument length
cmp.w #0,d0 ; compare d0 with zero - we leave the last argument character since it's just a return
bne copyarg ; if d0 != 0 then goto copyarg
bsr opendos ; branch to subroutine opendos
move.l d7,d0 ; move d7 into d0 (restore argument length)
lea.l argbuffer,a0 ; move argbuffer address into a0
bsr writechar ; branch to subroutine writechar
rts ; return from subroutine
noarg: ; handling that no arguments was entered in CLI
rts ; return from subroutine
argbuffer:
blk.b 80,0 ; allocate 80 bytes to argbuffer
writechar: ; writechar subroutine. writechar(a0,d0)
movem.l d1-d7/a0-a6,-(a7) ; push register values onto the stack
move.l a0,a5 ; move a0 (argbuffer) into a5
move.l d0,d5 ; move d0 (arg length) into d5
lea.l txt_dosbase,a0 ; move txt_dosbase address into a0 (contains base address of dos.library)
move.l (a0),a6 ; move base address of dos.library into a6
jsr -60(a6) ; call Output in dos.library. d0 = output()
move.l d0,d1 ; move d0 (filehandle) into d1
move.l a5,d2 ; move a5 (argbuffer) into d2
move.l d5,d3 ; mvoe d5 (arg length) into d3
jsr -48(a6) ; call Write in dos.library. d0 = Write(d1,d2,d3)
movem.l (a7)+,d1-d7/a0-a6 ; pop values from the stack into the registers
rts ; return from subroutine
opendos: ; opendos subroutine. opendos()
movem.l d0-d7/a0-a6,-(a7) ; push register values onto the stack
clr.l d0 ; clear d0
move.l $4,a6 ; move base pointer of exec.library into a6
lea.l txt_dosname,a1 ; move pointer to library name into a1
jsr -408(a6) ; call OpenLibrary in the exec.library. d0 = OpenLibrary(a1,d0)
lea.l txt_dosbase,a5 ; move address of txt_dosbase into a5
move.l d0,(a5) ; move dos.library base address into txt_dosbase
movem.l (a7)+,d0-d7/a0-a6 ; pop values from the stack into the registers
rts ; return from subroutine
txt_dosname:
dc.b "dos.library",0 ; library name terminated by zero
txt_dosbase:
dc.l $0 ; allocation for holding the base address of dos.library
It’s important to run this program from the CLI, otherwise it won’t work. To compile the program and make an executable, write the following in Seka.
SEKA>a
OPTIONS>
No Errors
SEKA>W
FILENAME>mc1004
Then go into the CLI and run the program and see it echo back the arguments
1.amigahd:DISK1/BREV10>mc1004 cheers mate
cheers mate
1.amigahd:DISK1/BREV10>
It works! Cheers! 🍺
Next we’ll tie it all together in a larger program.
Scroller revisited
Now it’s time to combine the subroutines, so that the scroller program we took a look at back in a previous post, can be upgraded to take advantage of our new knowledge.
The mc1005 program upgrades mc0701, so that it now is expected to run from the CLI taking a filename as argument. The file will contain the text that should be scrolled across the screen. No more recompilation because of spelling mistakes - yay 🚀
I won’t comment all of the code. Much of it has been described before. For the orignal scroller, check out mc0701 described in this letter VII post.
Here’s the code for mc1005 with my comments added, where the code differs from mc0701. The code is also found on disk1.
cmp.w #1,d0 ; compare d0 (CLI argument lenght) with 1
bgt argok ; if d0 > 1 then go to argok (we ignore carriage return)
rts ; return from subroutine (no arguments)
argok: ; arguments present
lea.l filename,a1 ; move filename address into a1
copyargloop: ; copy arguments to a1 (filename)
move.b (a0)+,(a1)+ ; move arguments that a0 points at, to what a1 points at, then post increment
subq.w #1,d0 ; subtract d0 (argumnent length) by 1
cmp.w #1,d0 ; compare d0 with 1 (have we reached the end?)
bne copyargloop ; if d0 > 1 go to copyargloop
move.l #50000,d0 ; move 50000 to d0 (number of bytes to allocate)
bsr allocdef ; branch to subroutine allocdef. d0 = allocdef(d0)
cmp.l #0,d0 ; compare d0 with 0 (check return value from allocdef)
bne memok ; if d0 != 0 then goto memok
rts ; return from subroutine (memory error)
memok: ; memory ok
lea.l buffer,a1 ; move buffer address into a1
move.l d0,(a1) ; move d0 (points to allocated memory) into address pointed to by a1 (buffer)
lea.l filename,a0 ; move filename address into a0
move.l d0,a1 ; move d0 (points to allcoated memory) into a1
move.l #50000,d0 ; move 50000 into d0
bsr readfile ; branch to subroutine readfile. d0 = readfile(a0,a1,d0)
cmp.l #0,d0 ; compare d0 with 0 (check return value from readfile)
beq freeup ; if d0 = 0 then goto freeup (no bytes were read)
move.w #$4000,$dff09a
or.b #%10000000,$bfd100
and.b #%10000111,$bfd100
move.w #$01a0,$dff096
move.w #$1200,$dff100
move.w #0,$dff102
move.w #0,$dff104
move.w #2,$dff108
move.w #2,$dff10a
move.w #$2c71,$dff08e
move.w #$f4d1,$dff090
move.w #$38d1,$dff090
move.w #$0030,$dff092
move.w #$00d8,$dff094
lea.l screen,a1
lea.l bplcop,a2
move.l a1,d1
swap d1
move.w d1,2(a2)
swap d1
move.w d1,6(a2)
lea.l copper,a1
move.l a1,$dff080
move.w #$8180,$dff096
mainloop:
move.l $dff004,d0
asr.l #8,d0
and.l #$1ff,d0
cmp.w #300,d0
bne mainloop
bsr scroll
btst #6,$bfe001
bne mainloop
freeup: ; free memory
move.l #50000,d0 ; move 50000 into d0 (50000 bytes)
lea.l buffer,a0 ; move buffer address into a0
move.l (a0),a0 ; move value in a0 (points to allocated memory) into a0
bsr freemem ; branch to subroutine freemem. freemem(a1,d0)
move.w #$0080,$dff096
move.l $04,a6
move.l 156(a6),a1
move.l 38(a1),$dff080
move.w #$80a0,$dff096
move.w #$c000,$dff09a
rts
scrollcnt:
dc.w $0000
charcnt:
dc.w $0000
scroll:
lea.l scrollcnt,a1
cmp.w #8,(a1)
bne nochar
clr.w (a1)
lea.l charcnt,a1
move.w (a1),d1
addq.w #1,(a1)
lea.l buffer,a2 ; move buffer address into a2
move.l (a2),a2 ; move value in a2 (points to allocated memory) into a2
clr.l d2
move.b (a2,d1.w),d2
cmp.b #42,d2
bne notend
clr.w (a1)
move.b #32,d2
notend:
lea.l convtab,a1
move.b (a1,d2.b),d2
asl.w #1,d2
lea.l font,a1
add.l d2,a1
lea.l screen,a2
add.l #6944,a2
moveq #19,d0
putcharloop:
move.w (a1),(a2)
add.l #64,a1
add.l #46,a2
dbra d0,putcharloop
nochar:
btst #6,$dff002
bne nochar
lea.l screen,a1
add.l #7820,a1
move.l a1,$dff050
move.l a1,$dff054
move.w #0,$dff064
move.w #0,$dff066
move.l #$ffffffff,$dff044
move.w #$29f0,$dff040
move.w #$0002,$dff042
move.w #$0517,$dff058 ; changed from #$0523 to #$0517 by me (I suspect an error)
lea.l scrollcnt,a1
addq.w #1,(a1)
rts
readfile: ; the readfile subroutine (described elsewhere)
movem.l d1-d7/a0-a6,-(a7)
move.l a0,a4
move.l a1,a5
move.l d0,d5
move.l $4,a6
lea.l r_dosname,a1
jsr -408(a6)
move.l d0,a6
move.l #1005,d2
move.l a4,d1
jsr -30(a6)
cmp.l #0,d0
beq r_error
move.l d0,d1
move.l d0,d7
move.l a5,d2
move.l d5,d3
jsr -42(a6)
move.l d7,d1
move.l d0,d7
jsr -36(a6)
move.l d7,d0
movem.l (a7)+,d1-d7/a0-a6
rts
r_error: ; handle readfile error
clr.l d0 ; clear d0
movem.l (a7)+,d1-d7/a0-a6 ; pop values from the stack into the registers
rts ; return from subroutine
r_dosname:
dc.b "dos.library",0 ; library name terminated by zero
allocdef: ; the allocdef subroutine (described elsewhere)
movem.l d1-d7/a0-a6,-(a7)
moveq #1,d1
swap d1
move.l $4,a6
jsr -198(a6)
movem.l (a7)+,d1-d7/a0-a6
rts
freemem: ; the freemem subroutine (described elsewhere)
movem.l d0-d7/a0-a6,-(a7)
move.l a0,a1
move.l $4,a6
jsr -210(a6)
movem.l (a7)+,d0-d7/a0-a6
rts
copper:
dc.w $2c01,$fffe
dc.w $0100,$1200
bplcop:
dc.w $00e0,$0000
dc.w $00e2,$0000
dc.w $0180,$0000
dc.w $0182,$0ff0
dc.w $ffdf,$fffe
dc.w $2c01,$fffe
dc.w $0100,$0200
dc.w $ffff,$fffe
screen:
blk.l $b80,0
font:
blk.l $140,0
convtab:
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $1f ;" "
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $1f ;" "
dc.b $00
dc.b $00
dc.b $1b ;Ø
dc.b $1c ;Å
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $1d ;,
dc.b $00 ;-
dc.b $1e ;.
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $00
dc.b $1a ;Æ
dc.b $00 ;A
dc.b $01 ;B
dc.b $02 ;C
dc.b $03 ;...
dc.b $04
dc.b $05
dc.b $06
dc.b $07
dc.b $08
dc.b $09
dc.b $0a
dc.b $0b
dc.b $0c
dc.b $0d
dc.b $0e
dc.b $0f
dc.b $10
dc.b $11
dc.b $12
dc.b $13
dc.b $14
dc.b $15
dc.b $16 ;....
dc.b $17 ;X
dc.b $18 ;Y
dc.b $19 ;Z
dc.b $00
dc.b $00
dc.b $00
buffer:
dc.l 0 ; holds the pointer to the allocated buffer (holds contents of file)
filename:
blk.b 50,0 ; the filename
To compile the program and make an executable, write the following in Seka.
SEKA>a
OPTIONS>
No Errors
SEKA>ri
FILENAME>font
BEGIN>font
END>
SEKA>wo
MODE>c
FILENAME>scroll
SEKA>
Notice that the mode is set to c, which I think is for chip ram. I also tried f, which I guess is for fast RAM, but that won’t work since we have a screen buffer in the program, that needs to reside in chip ram.
I have called the executable for scroll. Now use it in the CLI like this:
amigahd:DISK1/BREV10>scroll text
You should now see some nice scrolling text 😃. Remember that hitting the left mouse button quits the program.
Something Odd About the Size
When looking at the BREV10 folder in disk1, I noticed that the mc1005 executable was only 2.304 bytes, while my scroll executable was a whopping 14.076 bytes.
I think the folks, that prepared disk1 had to conserve the scarce 880 kB space, there is on an Amiga disk. It looks like they did not allocate the screen buffer in the program, but just used e.g. the allocchip subroutine.
screen:
blk.l $b80,0
This allocation of the screen buffer within the program is 11.776 bytes. From the letter VII post, we know that the screen is 23 words times 256 lines, and that’s exaclty the size of the screen buffer. $$23 words * 2 * 256 lines = 11.776 bytes$$
Funny enough, that’s almost the difference between the sizes of the executables. $$14076 bytes - 2304 bytes = 11.772 bytes $$
I would not have expected it to match exactly, since we need some extra calls to allocate memory for the screen.
Since it’s so easy to allocate memory, we should really not allocate large buffers within the program, since it only creates bloated executables. Another benefit, is that we can place our program in fast ram, while allocating buffers in chip ram, that can be accessed by the custom chips. In this way we save some of the scarce chip ram.
Previous post: Amiga Machine Code Letter X - Files.
Next post: Amiga Machine Code Letter X - More CLI.