LESSON NINE
It has been pointed out to me, by an astute reader, that I neglected to tell you to run your MC routines in SLOW, if you are using the TS1000. Otherwise, you cannot see the display of any of my examples. Sorry about that.
This time we will discuss the I/O instructions. For those of you that are wondering what I/O means, it is Input and Output. When I was new to computerdom, I thought I/O referred to my financial status.
To what are we Inputing and Outputing? The computer, but it is actually our old friend, CPU. The I/O instructions allow the CPU to receive or send information through the concept of PORTS and they accomplish this depending upon how the manufacturer made the hardware surrounding the CPU. For example, in our computers port FEh is used for the keyboard. There are others used by Sinclair for the 2040 printer, cassette, and on the 2068, for the bank switching and video mode changes. These are ‘hard-wired’ in the computer and supported by the operating system, therefore we cannot change them.
What is a PORT? Very simply, it is the doorway though which information flows to and from the CPU and outside devices. There are 256 ports available to us on the Z80 (there are really 65,536, but we will not consider them here). The ports are of course numbered 0-255, as they must be referred to in a single byte. Think of each port as a door to a storeroom. Each door has a number on it, much like a motel would. Each storeroom can hold one byte of data at a time. The CPU can put data in or take data out, by referring to each port (door).
The I/O instructions are In and Out, respectively and there are two forms of the instructions, as detailed in the syntax chart. We are looking at some instructions that are almost english and fairly easily understood.
The forms In A,(n) and Out (n),A use the port specified by n and reads (In) data into A or writes (Out) data from A. This is very similar to the Basic In and Out commands, except that the data is stored in the accumulator. None of the flags are affected by these instructions.
For example:
In A,(FFh) reads port FFh and places one
byte of data into the accumulator
Out A,(FFh) writes one byte of data from the
accumulator to the device which
is addressed by port FFh
The forms In r,(C) and Out (C),r allow the flexibility of reading or writing data with any register. Caution, remember that C contains the port address. The Out (C),r instruction does not affect any flags, while the In r,(C) affects all the flags, except Carry, according to data which was read in.
Register C must be loaded with the port address, prior to use, as in these examples:
Ld C,FFh reads port FFh and places data
In B,(C) in the B register
Ld C,FFh writes data from the B register
Out B,(C) to the device addressed by port FFh
You will note the I/O instructions assume you are communicating with some device (printer, monitor, disk, etc.) which is ‘addressed’ by a port number. The port number is selected by the hardware manufacturer, just as Sinclair did in our computers. You can perform I/O operations on all ports however, the results are unpredictable without a device attached. This is due to lack of pull-up resistors on the data lines. Obviously, there will not be any communication if there is no device attached or an incorrect port number is used.
Since we have to contend with devices that are much slower than the CPU, we also have to consider timing. I will not get into this subject very deep, as this type of programming becomes very hardware dependent.
The timing problem is obviously one of slowing down the I/O operations, in an effort to match the device. Let’s consider the simple case of reading a switch. We might wish to read the switch once per second, to eliminate multiple switch closures (a good example is in debouncing the keyboard switches).
We can perform this type of delay by looping for a predetermined time period. A simple delay routine that can be used, without destroying any registers is:
Delay Push BC ;save these registers
Ld B,xx ;xx = # of ms to delay
Dly1 Ld C,yy ;yy = 1 ms delay count
Dly2 Dec C ;loop for 1 ms
Jr NZ, Dly2
DJNZ, Dly1 ;loop for # of ms
Pop BC ;retrieve registers
Ret ;end delay
The value xx is the number of milliseconds to delay and yy is the number of loops needed for a delay of one millisecond. I will not take you through the steps of counting the delay as I wish only to demonstrate the technique. When you are ready to use this routine, you will not need my help with the values xx and yy.
Another method of delay can be used with ‘smart’ devices, such as a printer. This method uses two separate loops, instead of the nested loops we just looked at. Our example assumes the printer (actually, it’s interface) is wired for port 7Fh and it sends a zero byte when ready to accept data.
Ready? Ld C,7Fh ;get port address
In A,(C) ;get ready status from printer
Jr NZ,Ready? ;loop unless zero byte received
Print Ld A,data ;get data byte to print
Out (C),A ;send data to printer
Ret ;end delay
This method has the advantage of not sending any data, unless the device is ready, therefore no data is lost. Can you imagine how this article would look, if some characters were lost in transit to the printer? No, that’s not what happened, I just write poorly.
There is another solution to this timing problem, which uses hardware. We will not discuss that here, but you should be aware of it.
You also note some I/O instructions on the chart that I have not explained. These perform block I/O operations and will be explained next time, with the rest of the block instructions. They are included here so that it will be clear they are I/O instructions.
By now, many of you are undoubtedly trying to write your own MC programs. I wish to give some tips and hints, that will make the process less painful.
First, DO NOT attempt to write a large MC program on the first try. Instead, take the approach we have followed here and write small routines that do a specific job. They can be easily called from Basic and will return to the next Basic line. I would suggest you take a small working Basic subroutine and try converting it to MC. An arithmetic routine is the easiest to convert, as long as it does not contain special functions, such as SQR, COS, etc.
Write your MC in modules (subroutines) that can be easily tested and debugged. This also allows you to develope a library of known, debugged routines that can be used again. Look closely at the routines I have provided in this series. You will note that they are very similiar to each other.
I do not flow-chart and will not describe that to you. There are many good books on the subject. However, before you begin codeing your routine, there are some questions you need to answer or data to collect:
1)Purpose -what do we hope to accomplish? 2)Examples -what happens if? try several tests 3)Inputs -what data does routine need upon entry? 4)Outputs -what data is returned to calling program?
I also strongly encourage you to document you program. All of us have purchased programs that were not user friendly, in spite of it’s claims, and in addition, had no documentation. This is deplorable, but the biggest reason for documenting you own programs is for ease of use. I have written code, been interrupted, and when I returned to it a few months later, I could not determine what the code did or why I wrote it that way! Some essentials to proper documentation include:
1)Purpose -if the above questions were answered,
they should be included here
2)Registers -which ones are used? which ones are
destroyed and which are preserved?
what should they contain upon entry
and exit?
3)Inputs -what data does routine need upon entry?
4)Outputs -what data is returned to calling routine?
5)Routines Callled By -what routines use this one
as a subroutine?
6)Routines Called -what routines does this one call?
7)Commented Source -an absolute necessity
This is not the only information that should be in your documentation, but it is enough to make that routine useful to you next time around. Without this information, you will not develope a useful library of routines and will continually need to reinvent the wheel. If you follow these suggestions, you will find MC programming easy (well, almost) and if not, you will soon give up in frustration.
Now, how about a short routine? Let’s convert binary numbers to decimal digits for printing.
The easiest way to accomplish this is by repetitively subtracting powers of ten from our binary number and counting the number of times the subtraction is possible, better known as division. For the more advanced, try doing this by using the shift and rotate instructions. I am using the subtraction technique, as the code is much easier to follow.
;Set-up Demonstration
;********************
;
;Inputs: none
;Outputs: print decimal number
;Routines Called: Bn2Dec
;Routines Called By: none
;Purpose: set-up hl for our conversion routine
;
Org 7530h
Set-up Ld HL,4000h ;hl=number to convert
Call Bn2Dec ;go convert it
Done Ret ;converted and printed
;this is our return to
;basic
;
;
;our routine really begins here
;
;Convert Binary to Decimal
;*************************
;
;Inputs: HL=Binary Number
;Outputs: decimal number is printed
;Routines Called: Divide
; Print
;Routines Called By: Set-up
;Purpose: convert binary number to decimal
; ascii characters for printing
;
Bn2Dec Ld BC,D8F0h ;-10,000
Call Divide ;go get 10^4 digit
Ld BC,FC18h ;-1,000
Call Divide ;go get 10^3 digit
Ld BC,FF9Ch ;-100
Call Divide ;go get 10^2 digit
Ld BC,FFF6h ;-10
Call Divide ;go get 10^1 digit
Ld A,L ;a = 10^0 digit
Exit Jp Print ;go print 10^0 digit
;
;
;Divide by 10^x
;**************
;
;Inputs: HL=Binary Number
; BC=10^x
;Outputs: A=decimal digit to print
;Routines Called: Print
;Routines Called by: Bn2Dec
;Purpose: divide binary number by power
; of ten to obtain decimal digit
; by repetitive subtraction
;
Divide Xor A ;clear our counter
DvLoop Add HL,BC ;perform subtraction
Inc A ;count it
Jr C,DvLoop ;do again if possible
Sbc HL,BC ;otherwise adjust the
Dec A ;counters for the extra
;subtraction
Ret Z ;division not possible
DvDone Jp Print ;go print it
;
;
;Print Ascii Character
;*********************
;
;Inputs: A=decimal digit
;Outputs: digit in A is printed
;Routines Called: Rom Print
;Routines Called By: Bn2Dec
; Divide
;Purpose: call rom print routine while
; preserving the registers
;
Print Push HL ;save all registers
Push BC
Add A,30h ;2068 only
;make an ascii character
Add A,1Ch ;1000 only
;make an axcii character
Rst 10h ;go print digit in A
Pop BC ;restore registers
Pop HL
PrDone Ret ;digit has been printed
;
End
Chart 1 ----------!---------- In A,(n) ! Out A,(n) In r,(C) ! Out (C),r Ini ! Outi Inir ! Otir Ind ! Outd Indr ! Otdr
There are several things to note. First, since each routine is a separate module that could be called from anywhere, there are a few unneccessary instructions. For instance, the Jp Print is not needed at the DvDone label, as the Divide routine could simply ‘fall through’ to Print. I used Jp to demonstrate the use of another routine’s Ret instruction, in place of a Call Print and the subsequent Ret that would have been needed to end the Divide routine. Assume for a moment that Print does not follow immediately behind Divide. Try to follow the program through and see that the Divide routine uses the Ret instruction from the Print routine to return to the main routine, Bn2Dec.
Also, I used the Rst 10h rom print routine for compatibility on both the 1000 and 2068. Use of the rom routines often destroys the registers, therefore they were saved. Even BC, which we could have discarded.
The source is written along the quidelines given above. You should note that the comments do not echo the instructions, except when it serves to clarify. I have seen many listings that look like:
Ld HL,4000h ;HL=4000h
Obviously not very informative or useful.
Several lessons ago, I made the rather obnoxious claim that all arithmetic could be performed with addition. This routine will perhaps clarify that statement. We needed to divide. We chose to subtract, to achieve this. We chose to add a negative number, in lieu of subtraction. We divided!
As a friend of mine says, “Th-Th-Th-That’s all folks!”, that is, until next issue.
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.