CP/M Assembly Language Part III: Calling BDOS by Eric Meyer Last time we learned about the structure of the 8080 CPU, and wrote a simple little program to move around bytes (or words) of data with it. We found, however, that we couldn't tell we had done anything. Now we need to learn how to communicate with the terminal, print messages, and get keyboard input. 1. The CALL and JMP instructions It's good practice to divide assembler code into separate modules. As you probably can imagine, once you have a few dozen (or hundred) lines of code, things start looking disorganized. Then, of course, you need to be able to run each module of code in the proper sequence. By nature, the 8080 CPU just goes along executing one instruction after another, as it finds them. But there is a pair of instructions that cause it to go and execute code at some other location instead. The JMP (jump) instruction is like GOTO (a branch) in many languages; it simply goes and begins executing code someplace else. The CALL instruction, like GOSUB (a subroutine call), does the same thing, but remembers where you were before, and will return there when it finds a RET instruction. Both JMP and CALL take an address (16-bit data value) for an argument. This is almost always a label marking the beginning of the code you want to execute. For example: INSRT1 INSTR2 JMP LABEL INSTR3 LABEL: INSTR4 INSTR5 actually will execute instructions 1, 2, 4, 5, skipping instr3. Similarly, INSTR1 CALL LABEL INSRT2 RET LABEL: INSTR3 INSTR4 RET will execute the instructions in the order 1, 3, 4, 2. As an exercise, you might want to go back to the byte mover program of the last installment, and rewrite the instructions that move a byte from source to destination as a subroutine, which you could then CALL any number of times in sequence. 2. BDOS Calls In Part I, I said that assembler differed from higher-level languages in that there were no prepared subroutines for you to call, you had to write every byte of code yourself. Actually, this isn't strictly true. CP/M has a number of routines in the BDOS (basic disk operating system) which are designed to be called as subroutines, and allow you to do useful things like send characters to and from the terminal, read and write disk files, etc. This is the only way to do these tasks without knowing a lot of specific detail about (or duplicating a lot of the code in) the BIOS (basic input/output system) that allows CP/M to run on your particular computer. In brief, the operating system has already been told how to do these things -- all you have to do is ask it to do them. There are several dozen "BDOS calls". At address 0005H in memory, there is always a JMP instruction which leads into the operating system. Thus you make a BDOS call by setting up certain initial values, and then CALLing address 0005H. First you need to decide which BDOS function you want. Each has a code number, which has to be loaded into the C register. Then, if necessary, you put some additional information into the D-E registers. Then you CALL 0005H. The whole thing winds up looking like: MVI C,xx ; Put the BDOS function number here LXI D,xxxx ; (may need some more data here) CALL 0005H ; Ask the BDOS to do it Our immediate need is to communicate with a program. Here are three BDOS calls to do the job. Function 2 sends a character to the screen. You have to put the desired ASCII code into the E register (the D register isn't used here): MVI C,2 ; Console output function MVI E,xx ; Put the character you want here CALL 0005H ; Ask BDOS to do it This could get tedious for a whole string. We could write a subroutine to use this call repeatedly, but for now let's be lazy. Function 9 sends a whole string of characters to the screen. You have to use the $ character to mark the end of the string, and to put the address where the string starts into the D-E pair. The following complete program, which you can assemble and run, prints out "Hello world!": ORG 0100H ; MVI C,9 ; Print string function LXI D,STRING ; Address of the string to print CALL 0005H ; Ask BDOS to do it RET ; STRING: DB 'Hello world!$' END Function 1 gets a character from the keyboard. It doesn't require any other information; you simply: MVI C,1 ; Console input function CALL 0005H ; Ask BDOS to do it and the BDOS will wait for a key to be pressed. The ASCII code of the key will be returned to your program in the A register. The following program will ask you a question and accept your response: ORG 0100H MVI C,9 ; Print string LXI D,QUESTN ; (this one) CALL 0005H MVI C,1 ; get key CALL 0005H RET ; Who cares? ; QUESTN: DB 'Are you excited? (Y/N) $' END At the point where this program RETurns, the ASCII value of the key you typed is in the A register. Sadly, we don't know how to examine or test this yet, so there's no more to do for now. 3. It Talks, It . . . Now we can rewrite our "byte mover" program from Part II, so that it uses a neat subroutine to move each byte, and tells us what it's doing. Let's call this BYTEMOV2.ASM: BDOS EQU 0005H ; Define the BDOS entry address ORG 0100H ; Code begins here MVI C,9 ; BDOS print string fn LXI D,HELLO ; Ident. message CALL BDOS ; Do it MVI C,9 LXI D,DEST ; Show the original string CALL BDOS LXI H,SOURCE ; Point to source LXI D,DEST ; Point to destination CALL MOVBYT ; Move one byte CALL MOVBYT ; Move the second byte MVI C,9 LXI D,DONE ; Explain that it's changed CALL BDOS MVI C,9 LXI D,DEST ; Show what it is now CALL BDOS RET ; All done, return ; ; Subroutine to move a byte ; MOVBYT: MOV A,M ; Fetch byte from memory XCHG ; Point to destination MOV M,A ; Store byte XCHG ; Point back to source INX H ; Increment source INX D ; Increment destination RET ; All done ; ; Data area... note "$" char so we can use BDOS 9 ; SOURCE: DB 'Hi$' ; Move bytes from here DEST: DB '??$' : To here ; ; Messages ; HELLO: DB 'Byte mover program 2-- string was: $' DONE: DB ' and it is now: $' END When you assemble and run this program, you will know that it actually did something; what you should of course see is: A>BYTEMOV2 Byte mover program 2 string was ?? and it is now hi. Note how easy it would be to increase the string length -- just add in the extra characters at SOURCE and DEST, and call MOVBYT a few more times. 4. Coming Up . . . In future installments we'll learn some more assembler instructions, including arithmetic and conditional branches.