Perceptions

See all articles from SYNC v1 n5

Currently Technical Services Manager, David Ornstein has been with Sinclair since the opening of its U.S. office. He has been involved with Sinclair’s technical hotline, technical writing, and machine servicing. His primary interests are in the areas of software and hardware R & D, and system integration.

His secondary interests are reading (Frank Herbert), listening (Pink Floyd) and sharing perceptions.

A ROM Munching Session

Overview

One important aspect of a computer is its I/O (Input/Output) system. The I/O functions provided by a given computer system govern the usefulness of that system. The ZX80 is slightly limited by its storage system. With a ZX80, you can SAVE a program and LOAD it back in again. Granted, your variables are SAVEd (and LOADed) with your program, so you do have some limited data storage. But, imagine a program which takes an independent set of data (e.g., a text file), operates on it (e.g., a text editor or compiler), and produces some arbitrary output (e.g., a machine code program). This type of program is sometimes known as a filter. The difficulty is that you have no way of getting the independent data to the program.

One solution to this problem is to have a copy of the filter (program) for each set of data. This is not a very efficient method, but it does have the advantage of simplicity. However, I believe that a user should have a choice between filter copying and independent data storage. Then he can make his own trade-off decisions, thus utilizing the better method for his application.

In this issue, I will take you on a journey through the ZX80’s storage system. I will show you how to utilize the LOAD and SAVE routines in the ZX80’s ROM (Read-Only Memory), and apply them wherever, and however, you want. But first I will run through, in detail, the algorithm(s) used by the ZX80’s cassette system. For the greatest level of understanding you should follow the code in Listing 1 very carefully.

Listing 1.

Label     Assembly                 Comment

; Copyright (c) 1981,
by Sinclair Research Ltd.
SAVE: ; SAVE RAM
;
; Address=01B6 hex
0438 decimal
;
; Save from 4000h to (E.LINE)
;
POP DE ; remove return address
LD DE,4811 ; lead in
LSAV2: LD A,7Fh ; KB mask
IN A, (OFEh) ; set output to zero and
; read BREAK key
LSAVO: RRA ; move BREAK into CY (carry)
JR NC,LS8 ; if BREAK pressed
DJNZ $ ; wait 255*13+63 T-States
; (1.04 ms) each time around
; the LSAV2 silence loop;
; i.e., 5 seconds in all
DEC DE
LD A,D
OR E
JR NZ,LSAV2 ; if DE≠0
LD HL,4000h ; now dump RAM
LSAV27: LD DE,8+(248,sh1.8) ; i.e., e=8,d=248
LSAV3: RLC (HL) ; get next bit
SBC A,A ; FF if a 1, 00 if 0
AND 5 ; 5 if a 1,0 if 0
ADD A,4 ;
LD C,A ; c:=9 if a 1, 4 if a 0
LSAV4 OUT (OFFh),A ; tone burst
ID B,36 ; DELAY
DJNZ $ ; LOOP
LD A,07Fh
IN A, (OFFEh) ; set output to zero
; and read BREAK key
LD B,35 ; DELAY
DJNZ $ ; LOOP
DEC C
JR, NZ, LSAV4
LD B,D ; get timing for IB gap
LSAV5: NOP ; inter-bit gap
DJNZ LSAV5
LD D,254
DEC E
JR NZ. LSAV3 ; if more bits
RRQ
JR NC,LS8 ; if BREAK pressed
CALL ENDBYT
JR LSAV27
ENDBYT:
; common code for LOAD and
; SAVE at end of byte.
; 72 T-States including call
; and return if returns.
INC HL ; bump pointer
EX DE, HL
LD HL, (E-LINE)
SCF ; carry = 1
SBC HL, DE ; HL=HL - DE - carry
EX DE, HL
RET NC ; unless end of area to be dumped
POP HL ' discard return addr from ENDBYT
LS8: JR NEWLIN
LOAD ; LOAD RAM
;
; Address = 206 hex
518 decimal
; LOAD from 4000h on
POP DE
LL 1: LD DE, 22290
LL2: LD 9,7Fh
IN A, (OFEh) ; first look for lead-in: read
RLA ; every 15.1 µs if 1's, 21.5 µs
RRA ; if 05, wait until get
; zeroes for 480 ms, abort to
JR NC, LS8 ; NEWLIN IF BREAK pressed
RLA ; CY = cassette data
JR C,LL1 ; if high then no leader yet;
; so go back
DEC DE
LD A,D
LD A,D
OR E
JR NZ, LL2 ; if DE≠0
INC (IY+E-LINE+1- ; in case is 40h now
4000h)
LD HL, 4000h
LL27: LD E, 8
LL3: LD A, 7F
IN A, (OFEh) ; get cassette/BREAK data
RRA ; put it in CY (BREAK, that is)
JR NC, LL7 ; if BREAK
RLA
RLA ; CY = cassette data
JR NC, LL3 ; read every 16 µs until a 1 is
; found
L D C, 148
LL4: LD B, 26 ; req'd 408 µs trailer after
; tone burst
LL5: DEC C ; count 15.4 µs if a 1, 15.2 if a 0
IN A, (OFEh) ; N.B. C is decremented at least
; 26 times, i.e., to 122 which
; has bit 7=0
RLA
BIT 7,C ; test bit 7
LD A,C
JR C, LL4
DJNZ LL5 ; until end of trailer then
; exit with carry clear
JR NZ, LL6 ; if>≈2.3 ms, is a 1
CP 86 ;
JR NC, LL3 ; if <≈0.8 ms, is noise
LL6: CCF ; here if data bit: carry set
; if 0, clear if 1
RL (HL) ; shift bit into dest
DEC E
JR NZ, LL3 ; go get next bit
CALL ENDBYT ; here after 8 bits
JR LL27
LL7: DEC D
JP P-0 ; if have been through ENDBYT
DEC (I Y+E - LINE ; else restore E-LINE
+1 - 4000h)
JR LS8

Save

The first thing the SAVE routine does is to POP a value off the stack. This is the address of the instruction after the CALL to SAVE. Since SAVE does not ever return there (it either goes to NEWLIN [the section of the ROM that gets a new line of Basic] or to NEW), it does not want to leave the stack cluttered with its RETurn address.

By listening to a SAVEd program on tape, you will notice that the first element of a SAVEd program is a 5 second silence. This is generated by the next section of code. The DE register pair is loaded with 4811 (decimal), the number of times to execute the “silence loop.”

The silence loop runs for 1.04 microseconds (millionths of a second) each time around, and its algorithm is as follows: First, load the keyboard mask to read the BREAK key. (See SYNC 1:3, “The ZX80 Keyboard,” for details on the ZX80’s keyboard system.) Next, read the keyboard/cassette input port. This has the effect of resetting, to zero, the output to the cassette port. The RRA instruction, at LSAV0 is used to rotate bit 0 of the input register, A, into the carry flag. This bit represents the BREAK key data. Then, if NC (No Carry) is true, the routine jumps to LS8, the abort section of the code, because BREAK was pressed. The DJNZ instruction is used to create a delay. Finally, the loop counter, DE, is decremented and compared with 0. If it is zero, then SAVE has executed the loop 4811 times (i.e. ≈, 5 seconds). Otherwise, the computer will proceed back, to LSAV2, and execute the loop again.

Now we are ready to SAVE RAM (Random-Accesss Memory). Here things start to get a bit hairy. The next section of code begins by picking up the start address for the SAVE, 4000h, and putting it in the HL register pair, the address pointer. The character loop begins here. It starts by setting D=8 (i.e., the number of bits in a byte). Now, set E=248 (i.e., the loop constant for the inter-bit gap [see later]).

The bit-save loop starts next (LSAV3). It proceeds as follows: First, execute an RLC (HL) instruction. This rotates the memory location pointed to by HL, left via the carry flag (i.e., everything shifts left one bit; bit 7 moves into the carry flag; bit 7 moves into bit 0). This operation is depicted in Figure 1.

       +-------------------------------+
       |                               |
[CY]<--[ 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 ]<-+
              <------------ (HL)
Figure 1. The RLC (HL) instruction.
Figure 1. The RLC (HL) instruction.

Notice that the bit-save loop executes 8 times, so the computer cycles through all the bits of the source once, and returns the location pointed to by HL to its original value when done with that byte. Next, execute an SBC A,A (SuBtract with Carry) instruction. In a nutshell, this instruction says: set A=A-A-carry; i.e., A=-1 if the bit, held in carry, is a 1, and A=0 if the bit is a 0. (N.B., -1d =FFh. Also -1d =1111 1111b.) So now we have A=0000 0000b if the bit we are SAVEing is a 0, otherwise A=1111 1111b. Execute an AND 5 instruction. This sets A equal to 0 or 5. Now, ADD A,4 (add 4 to A). Finally copy A into C with an LD C,A instruction.

At this point C=9, if we are SAVEing a 1 bit, and C=4 if we are SAVEing a 0 bit. This number is the loop constant for the “tone loop.” Basically, the tone loop’s algorithm is:

1) Set cassette output=on (Create a tone).
2) Delay.
3) Set cassette output=off. Read the BREAK key.
4) Delay.
5) Decrement C (the number of times to execute the tone loop).
6) If C≠0 then go back to step 1.

For a full understanding of how the hardware generates a tone, a detailed study of the ZX80’s schematic diagram is required. That is, however, beyond the scope of this article.

We continue the bit-save loop, by loading B with E (the inter-bit gap loop constant), and performing NOP (NO oPeration) instructions. (The DJNZ instruction is a special Z80 looping instruction. It says: decrement B and JR (Jump Relative) to some arbitrary memory location (LSAV5, in this case) if B≠0.) Finally, decrement D, the number of bits in this byte, to SAVE and jump back to LAV3 if D≠0.

If you look back and review the tone loop, you will notice that step three not only resets the cassette output, but also reads the BREAK key. This leaves bit 0 of A=0 if BREAK was pressed. The next two instructions, RRA (Rotate Right Accumulator) and JR NC,LS8, move bit 0 of the accumulator into the carry flag, and jump to LS8 if NC (i.e., BREAK was pressed).

SAVE nears completion by CALLing the subroutine ENDBYT, which increments HL, the pointer to the byte to SAVE, and, if there is more memory to SAVE, i.e., HL<(E-LINE), it returns. If ENDBYT finds there is no more memory to SAVE, it jumps to NEWLIN, which is the main section of the 4K Basic. If ENDBYT RETurns to SAVE, the instruction after the CALL ENDBYT, a JR LSAV27, causes the computer to go back and SAVE another byte.

Load

LOAD also begins with a POP operation, used to remove its return address from the stack, thus insuring clean operation. At LL1, DE is loaded with 22290d. This is the loop constant for the leader loop. The leader loop proceeds as follows:

1) Read the keyboard/cassette port.
2) If BREAK is pressed, jump to LS8.
3) If cassette port = one then go back to step 1.
4) Decrement DE (the loop constant).
5) If de≠0 (i.e., not 5 secs. of silence yet) then go back to step 1.

In essence, this loop searches for about 5 seconds of silence on tape. If a one is found during this time, it goes back and resets DE, the loop counter, thus reinitiating the 5 second search.

If BREAK is presed during this period, the LOAD routine jumps to LS8, which, in turn, jumps to NEWLIN. This allows you to change your mind about LOADing a program without deleting the program currently in memory. Hitting BREAK after the first byte has been transferred from cassette into RAM, however, is a destructive action.

Now we are almost ready to start LOADing into RAM. The final pre-LOAD step is to increment the high byte of E-LINE. This is done, in case this byte is 40h, to assure that ENDBYT permits at least 256 bytes to be loaded. Once this first page (256 bytes) of memory is LOADed, E-LINE, a system variable which resides in the first page of RAM, will have been overwritten with the appropriate value.

The routine at LL7, which is where LOAD jumps if BREAK is pressed after the silence loop finishes, tests to see if LOAD has been through ENDBYT, i.e., at least one byte has been LOADed. If it finds that LOAD has not called ENDBYT yet, then it permits the BREAK to be non-destructive. After decrementing the MS (Most Significant) byte of E-LINE back to its original value, it jumps to LS8, and thus back to NEWLIN. If ENDBYT has been called, the LS8 routine jumps to location 0, effectively resetting the computer.

Finally, we can begin to LOAD data into RAM. LOAD continues by loading HL, the destination byte-pointer, with 4000h (16384d) (i.e., the address of the first RAM location). Next, E is set to 8: the number of bits in a byte. The character loop begins with this instruction (LL27). The bit loop begins next (LL3).

The first four instructions of the bit loop are used to read the cassette/keyboard port and, if BREAK is pressed, jump to LL7. Next, two RLA (Rotate Left Accumulator) instructions are executed to rotate the cassette data into the carry flag. If the carry flag is clear (i.e., the cassette port was at logic 0), then LOAD jumps back to LL3. This section of code, by reading the cassette port every 16 microseconds, waits until 1 is found. This 1, which represents a tone burst, marks the beginning of a bit frame.

As soon as 1 is found, the loop falls through, and C is set to 148d. C is used to count the sum of the durations of each pulse and their trailers found during this bit frame. When the bit loop finally falls through, after the LL5 instruction, the C register is checked to see how long the input was on. If bit 7 of C is reset (i.e., C>≈2.3 ms), the JRNZ,LL6 instruction jumps to the routine at LL6 which shifts the bit into the destination. Otherwise, if C < 86 (i.e., C<≈.8 ms) the input was noise, so go back to LL3 and continue looking for the next bit. If the JR NZ, LL3 instruction (used to test for noise) falls through, the LL6 routine executes.

In LL6 an RL(HL) instruction is used to rotate the bit found on the cassette port into the destination. This is followed by a DEC E instruction and a JR NZ,LL3. This pair is used to go back and get the remaining bits for this byte. When E=0, the current byte has been filled, the bit loop falls through, and ENDBYT is called. If ENDBYT comes back, a JR LL27 is executed and the computer goes back for the next byte.

Reviewing the LOAD and SAVE routines, you will notice in each, an LD HL,4000 instruction. This instruction loads HL, the byte pointer, with 16384d (4000h), the address of the first memory location to SAVE or LOAD. Also, looking at ENDBYT will reveal that the system variable E-LINE holds the address of the last-byte-to-SAVE (or LOAD) plus 1. These two values, the 4000h in the LD instruction, and the contents of E-LINE, can be modified to allow any area of memory to be SAVEd or LOADed. My modification to these routines, to make the SAVE/LOAD area fully programmable, is to change the LD HL,4000h to an LD HL,x, where x is the address of the first memory location to SAVE from or LOAD to. Then, each time before you use the routines, POKE the appropriate value into E-LINE. The second part is easy. The first, however, is impossible (or almost). The difficulty lies in the fact that the LOAD/SAVE routines, and therefore, the LD HL,4000h instructions they contain, are in ROM. And you cannot change a ROM!

There is, however, a solution: copy the routines into RAM. Then, when you want to use the routines to SAVE/LOAD some section of memory, just POKE the start address into the two byte address field of the LD HL,xxxx insstruction. Next, POKE the address of the first byte not to SAVE/LOAD (i.e., the last-byte-address + 1) into E-LINE. Then, instead of using the LOAD or SAVE commands from Basic, use a USR(x) call to execute your modified versions of cassette handling routines in RAM.

This method of utilizing the 4K ROM’s SAVE and LOAD routines has many possible applications. You can SAVE and LOAD not only independent programs and data, but any area of memory. This means, for example, that you could write a program to selectively SAVE/LOAD some arbitrary (group of) variable(s). You could write a subroutine, for your text-editing system, that SAVEd a string array (i.e., a list of lines of text). You could then write a corresponding LOADer routine for your PASCAL compiler, which retrieved the text from tape. You could write a machine language monitor/debugger/editor (MDE) which allowed you to SAVE/LOAD machine language programs in any format you desire.

This capability of independent data storage has been missing from the ZX80’s repertoire for too long. I hope this article will give you the basics required to apply this powerful technique. If the reader response is great enough, some particular applications could be developed in a future column. Something I am working on, also perhaps for a future column, is a full fledged I/O processing system. It could handle reading and writing of records (blocks of data) in a cassette system. With a little bit of electronics, it could even control the motors in your tape recorder.

Video Sync Signals

Some owners have told me that they have tried my direct video circuit to drive a monitor (see ZX80 Technical Manual/SYNC 1:2, p. 12), apparently to no avail. The problem seems to be that some monitors require external sync sources. Vertical sync can be obtained from IC11/pin22 (for negative-going signals) and IC11/pin10 (for positive-going signals). Horizontal sync can be tapped from IC19/pin6 and IC19/pin5 for positive-going and negative-going pulses, respectively. These signals must usually be provided to separate terminals. Some monitors provide internal syncing as a selectable option. If yours does, use it.

Video Modulator Drive Circuitry

If you have a standard U.S. version ZX80, running on channel 2 (VHF), then your computer includes some extra circuitry for driving the video modulator. This information is not supplied with the ZX80 schematic. The circuit is shown in Figure 2.

Figure 2. The above correction applies to USA machines running on channel 2. It does not apply to channel 36 ZX80 kits. The transistor is used, in place of R32, to pull the composite video signal, running into the video modulator, low during sync time.
Figure 2. The above correction applies to USA machines running on channel 2. It does not apply to channel 36 ZX80 kits. The transistor is used, in place of R32, to pull the composite video signal, running into the video modulator, low during sync time.

The circuitry is used to superimpose negative-going sync pulses onto the video signal. Your schematic probably shows R2 used for this purpose. However, it is done this way only in the U.K. (channel 36, UHF) machines. R32 is not included in U.S. Channel 2 machines.

Loading

As you probably know, if not from experience, then by reading about it, the LOADing problem has been one of the greatest difficulties associated with ZX80s. In my dealings with machines, however, I have found that most of the units returned with a claim of “I CAN’T GET IT TO LOAD!!!!” work fine. Therefore, the assumption I must make is that the procedures being followed are incorrect, or, not being followed correctly. I will detail everything that Sinclair and I, as an individual, have learned about LOAD problems. The following six items are relevant to the situation.

1) Try cleaning and demagnetizing the record/playback heads of the tape recorder. Your ear may not pick up the signal fluctuations due to magnetic flux on the tape heads. The ZX80, however, has a much more sensitive “ear.”

2) Use a good quality tape. A “DATA TAPE” works well.

3) Check the output level of your tape deck’s EAR/SPKR jack. It must be at least 4 volts peak-to-peak (5 to 6v p-p is best). If you do not have a signal with enough amplitude, the ZX80 will continue looking for your program forever, and the TV screen will remain indeterminently gray indefinitely.

4) Do not use the output from a hi-fi amplifier, as this may damage your computer.

5) Try loading with only the EAR jack connected.

6) A DIN-type socket usually gives only about 1.5 volts peak-to-peak (or less). Therefore, it is unsuitable for use with your ZX80, unless equipped with an external buffer circuit.

Well, that’s it for this issue. Until next time. Same relativistic time period. Same non-Euclidian universe.