Micromusic (Part 1)
Make music with the ZX81
The Sinclair ZX81 is the cheapest BASIC microcomputer currently available, and many of our readers will no doubt own one. For the would-be computer musician, the machine has disadvantages, however. There is no sound generator or BASIC music statement, as on the Sharp MZ-80K; nor is there provision for an input/output port, or any statements for the use of a port. To cap it all, machine code programming has to be done via PEEK, POKE and USR, since there is no machine code monitor. Despite these objections, a little bit of work can yield quite amazing results, and the effort is well worth it; especially bearing in mind the cost of the computer, even including the 16K RAM pack which is necessary to obtain a decent amount of music storage. Before we can start thinking about I/O ports and driving synthesisers, however, there's something more basic to get to grips with.
Even if the ZX81 did have IN and OUT statements, all but the simplest sequencer programs would need to be in machine code for speed reasons: ZX81 BASIC simply isn't fast enough to haul vast quantities of data out of memory and feed it out, especially presto.
This article won't attempt to teach either ZX81 BASIC or Z80 machine code programming, since the ZX81 manual deals admirably with the former, and this magazine's Using Microprocessors series has dealt equally admirably with the latter; a reasonable knowledge of both will be assumed from now on.
Chapter 26 in the ZX81 manual gives three methods of entering machine code; the only one which I use is the REM statement, which has the advantages of (a) not moving about in the memory, provided it's always the first line in the program, and (b) it's saved on tape along with the rest of the program.
To elaborate on this, we'll jump in at the deep end. Enter this one line program:
1 REM TAN
Note that I've used Sinclair's convention of printing single key keywords, functions, etc., in bold type. These should be entered with the appropriate key, and not spelt out letter by letter. Now enter the following as a command, not another program line:
PRINT USR 16514
The answer will come up 16514. What's happened? Refer to the diagram at the bottom of page 171 in the ZX81 manual, which shows how a program line is stored. The first memory address to be occupied by the BASIC program is 16509 (in decimal): this is the line number of the first statement, which in fact always takes two bytes, i.e. locations 16509 and 16510. The 'length of text' bytes are at 16511 and 16512, and the words REM and TAN take one byte each, at 16513 and 16514 respectively. Our USR command called a machine code subroutine at 16514 - so for some reason it thinks the word TAN is a machine code program! Why?
The answer to this riddle lies in Appendix A, which lists the character set and several other useful things. Look up TAN, which is character code 201, and you will see that in hex this is C9 - the Z80 command 'RET' or 'return from subroutine', the shortest machine code segment you can write on the ZX81. All the program did, then, was to go to a machine code subroutine which told it to return to BASIC straight away; which it did, printing the value of the BC register pair which stayed unchanged at 16514. To convince yourself of this strange effect change your one line program to this:
1 REM AGENT
and enter the command
Now look at your program again and it will have changed to: 1 REM TAN GENT and if you try PRINT USR 16514 again you'll get the same answer. This gives us the simplest way of entering a machine code subroutine; without altering the BASIC program, enter the following series of commands:
This time the answer will come up 0. Decipher this new program using that handy character list, and in hex it should be:
which in Z80 assembler language is:
In other words, the program loads the BC register pair with zero, then returns to BASIC with this value. To avoid having to enter long sequences of POKEs to load every machine code program, try this simple program:
1 REM ..........
10 LET A = 16514
30 PRINT A,
40 INPUT D
50 IF D< 256 THEN GOTO 80
60 LET A = D
70 GOTO 20
80 POKE A,D
90 PRINT D
100 LET A = A+1
110 GOTO 20
The program starts at address 16514 and POKEs the decimal value you give it in to that address; then it steps onto 16515 and so on. If you want to skip on (or back) to a different address, enter the address first. As long as the number is greater than 255, the program will assume it's an address. To stop simply enter STOP. The REM statement, line 1, can contain any characters you like as long as there are enough to accommodate the program you want to enter; otherwise you'll start overwriting the rest of the BASIC program and the system will crash.
Many of you will want to write more intricate 'machine code monitors', and this is quite possible, although a simple program has advantages - it's easy to erase in order to superimpose another BASIC program which will make use of the machine code you've just written.
For people who are used to hex code, a simple refinement is possible, as in the next program:
1 REM ..........(etc.)
10 LET A = 16514
30 PRINT A,
40 INPUT D$
50 IF LEN D$ = 2 THEN GOTO 80
60 LET A = VAL D$
70 GOTO 20
80 POKE A, (16* (CODE D$(1)-28) + (CODE D$(2)-28)
90 PRINT D$
100 LET A = A+1
110 GOTO 20
This one accepts a two digit hex number - each digit must be between 0 and F inclusive. A 'change of address' request is detected if the input string is more (or less) than two digits - addresses are still entered in decimal. To finish entering, type RUBOUT then STOP.
Future programs will give machine code listings in hex code, along with Z80 assembler mnemonics. As an example, here is a routine which is vital in any machine code that loops round and round until you want it to stop. The program looks at the BREAK (SPACE) key and returns to BASIC if it is pressed. For the purpose of illustration, this routine simply loops back on itself; it would normally be incorporated as a segment of a longer loop, and the last instruction would be elsewhere in the program.
|16514||LD BC 7FFE||01 FE 7F|
|16517||IN A,(C)||ED 78|
|16521||JR 16514||18 F7|
At this point we need to examine the ZX81 hardware in a little more detail than the manual goes into. Figure 1 illustrates the keyboard matrix, which is arranged 5x8 internally although the actual board is 4x10. To address the space key, therefore, line A15 needs to be at a low level while A8-A14 remain high. Here we (and Sinclair) make use of a peculiarity of the Z80's port addressing system - or is it perhaps deliberate? When using indirect port addressing, register C is placed on the lower half of the address bus, representing the actual port address; meanwhile, register B appears on the upper half (A8 to A15) of the bus. So, if the port address to read the keyboard is FE, and 7F needs to appear on the upper half of the address bus (A15 low, remember) then a single LD BC instruction can set up both these conditions. The instruction IN A,(C) then reads the port FE into the accumulator. Since we only want to examine bit 0, 'rotate right accumulator' (RRA) will place this bit in the carry flag, where a conditional return may be done. Remember we're looking for a zero to indicate the key being pressed, so the return is performed if there is no carry (RET NC).
A word about port addressing: the ZX81 doesn't use a decoder for its input/output instructions; ports are addressed by the address bus directly. For example, the keyboard port is addressed by IORQ, RD and A0 being low simultaneously; so FE is obviously not the only address that could be used in this instance, any address which has A0 low will do. Similarly, A1 low is used by an internal port, and A2 low will address the printer. Any additional ports, then, should have addresses in which A0, A1 and A2 are high.
The rest of the hardware is fairly transparent, and it is possible to run a machine code program while SLOW mode is operating: but remember that it will be interrupted 50 times a second, so any programs with critical timing in them should be run in FAST mode.
The interrupt lines, INT and NMI, are both connected internally and so should not be used - even though they are available via the edge connector - unless tracks are cut inside the computer; not recommended procedure!
The last point concerns chapter 26's warnings about 'HALT' instructions; whilst it is true that the BASIC interpreter will translate these as a new line - and this applies to 76(hex) as data also - all that will happen is that the listing of your REM statement will look a bit peculiar. Any data or op code may be used without problems.
These articles are headed 'Micromusic' and so far, it's been all micro and no music. This will be rectified in the next article, where we shall explore interfacing, with particular reference to the driving of synthesisers.
It is not intended to present programs in which the ZX81 generates the sound itself; through the cassette port, say. We feel that the musical uses of a monophonic square wave are strictly limited, and yet such programs require a fair amount of programming effort which is not really worthwhile.
We have published similar programs in the past, however; if you would like to see one for the ZX81, please let us know and we will try to fit one in a future article.
Gear in this article:
Feature by Peter Maydew
mu:zines is the result of thousands of hours of effort, and will require many thousands more going forward to reach our goals of getting all this content online.
If you value this resource, you can support this project - it really helps!