Amiga Machine Code Letter X - CLI

Amiga Machine Code - Letter X

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.

file size

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.


Amiga Machine Code Course

Previous post: Amiga Machine Code Letter X - Files.

Next post: Amiga Machine Code Letter X - More CLI.

Mark Wrobel
Mark Wrobel
Team Lead, developer and mortgage expert