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.
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:
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.