Channels and streams
The Spectrum has a surprisingly modern system of input and output when the age of the Spectrum is considered. However, what is more surprising is the fact that the Spectrum manual barely scratches the surface of what is possible.
I/O on the Spectrum is based on channels and streams. Since the standard Spectrum has only a limited range of I/O devices it makes sense that different commands are available for each I/O device. For example, PRINT is used to send output to the screen, whereas LPRINT is used to send output to the printer.
The extra devices catered for by the ZX Interface I (microdrives, RS-232, and networking) reduced the practicality of continuing to invent new commands, although this was provided for in the case of the microdrives.
Streams and channels intuitively correspond to the software and hardware parts of I/O respectively. That is, a stream should be thought of merely as a collection of data going to or coming from a piece of hardware, and a channel should be associated with a particular piece of hardware such as a printer. On the Spectrum streams are numbered from 0 through 15, and their basic operations are reading and writing data.
The BASIC statement INPUT #s; [input-list] will read data from stream number s, 0 <= s <= 15, into the variables specified in the input-list. Conversely, the BASIC statement PRINT #s; [print-list] will write data to stream s, 0 <= s <= 15. In general both INPUT # and PRINT # can be used in the same way as their ordinary counterparts INPUT and PRINT. In particular all the normal complexity of a PRINT statement can be used equally well in a PRINT # statement. In each case the data sent to the stream is exactly the same as the data which would be sent to the screen by the PRINT statement. The INPUT # statement is slightly more complicated in that is can both read and write data, as in INPUT "What is your name? "; A$. In fact each stream really has two components, an input stream and an output stream. Data written to the stream by either PRINT # or INPUT # goes to the output stream while input comes from the input stream.
It is even possible to change streams part way through a PRINT statement, as in PRINT #3; "hello"; #6; "there". This is obviously fairly confusing though, so should probably be avoided unless there is a good reason for using this construct.
Opening and Closing
How do you know which stream numbers are associated with which channel? Before a stream is used it must be OPENed. Opening a stream serves two purposes. It associates the given stream with a particular piece of hardware (the channel), and actually signals the relevant device that it is going to be used. Stream are opened in BASIC using the syntax OPEN #s, c where s is the stream number being opened and c is a string specifying the channel to associate the stream with. Following this command, any data sent to stream s will go to the specified channel. It is possible to open several streams to the same device, but each stream can only be associated with a single channel.
The statement CLOSE #s is used to end the association of stream s with a channel. If you attempt to close a channel which is already closed on an unexpanded Spectrum then the machine might crash due to two bugs in the ROM.
The unexpanded Spectrum supports four channels: "K" the keyboard channel, "S" the screen channel, "P" the printer channel, and "R" an internal channel used by the Spectrum to send data to the edit buffer. In practice the only channel that has both input and output of these is the keyboard channel. When the Spectrum is first powered up the following streams are opened automatically: 0: "K", 1: "K", 2: "S", 3: "P". Thus, the command LPRINT is really an alternative to writing PRINT #3. The "R" channel cannot be opened from BASIC. It is possible to redefine the standard channels, thus OPEN #2, "P" will cause output normally sent to the screen to be redirected to the printer.
For example, OPEN #5, "K" associates stream 5 with the keyboard, and thereafter INPUT #5; A$ would behave in an identical manner to INPUT A$.
The most important advantage of using streams is in the writing of device independent programs. Say that you wish to give the user the option of having all output go to either the screen or to the printer. Without using streams it is necessary then to have separate output statements for each device, as in;
IF (output = printer) THEN LPRINT "Hello" ELSE PRINT "Hello"
By using streams we can just open a particular stream (say 4) to the desired output device and thereafter use only one output statement;
PRINT #4; "Hello"
Obviously this will result is a much shorter program, particularly, if there are many output statements in the program. Further, it is an easy matter to add even further output devices if they become an option later in the programs development.
You could also hack an existing program by adding somewhere near the start;
IF [printer required] THEN OPEN #2,"P"
which will make all ordinary PRINT instructions go to the printer.
More Stream Commands
BASIC also allows LIST and INKEY$ to be used to streams. LIST #s will send a copy of the program to stream s; e.g. normally LIST #3 is the same thing as LLIST. However, on the standard Spectrum INKEY$ can only be used with the keyboard channel.
Note that INKEY$ is not the same as INKEY$ #1 because the former does a stand-alone key scan whereas the latter attempts to read a key from the "K" device (which might involve changing the cursor mode and listing the editing area if you happen to press both shifts, for example).
Knowing about the actual layout of the stream records in memory is useful if you want to add your own hardware devices to the Spectrum, or if you which to make you own specialized streams. The information that defines each channel is stored in the channel information area starting at CHANS and ending at PROG - 2. Each channel record has the following format:
two-byte address of the output routine, two-byte address of the input routine, one-byte channel code letter.
where the input and output routines are address of machine code sub- routines. The output routine must accept Spectrum character codes passed to it in the A register. The input routine must return data in the form of Spectrum character codes, and signal that data is available by setting the carry flag. If no data is available then this is indicated be resetting both the carry and zero flags. Stubs should be provided if a channel does not support either input or output (e.g. the stub may simply call RST 8 with an error code)
With the ZX Interface I attached, an extended format is used;
offset len description 0 2 0x0008 (address of Spectrum error routine) 2 2 0x0008 (ditto) 4 1 A character describing the channel 5 2 Address of output routine in the shadow ROM 7 2 Address of input routine in the shadow ROM 9 2 Total length of channel information 11 * Any other data needed by the channel
The first two words being 8 denotes that the input and output routines are to be found in the shadow ROM. Either of them could be an ordinary number instead, in which case the shadow ROM will not be paged in and the word at offset 5 or 7 (as appropriate) could contain any data.
Data about which streams are associated with which channels is in a 38-byte area of memory starting at STRMS. The table is a series of 16-bit offsets to the channel record vectored from CHANS. A value of one indicates the channel record starting at CHANS, and so on. This accounts for 32 bytes of the 38 - the remaining 6 bytes are for three hidden streams (253, 254, 255) used internally by BASIC. A zero entry in the table indicates a stream not open.
It is possible to redirect existing channels to your own I/O routines. This can be used among other things to cause LPRINT to use your own printer driver rather than the one provided in the ROM. It allows you to perform I/O for your own hardware devices, or for you to write your own handlers from PRINT and INPUT.
It is easiest to modify the existing "P" channel record. The "K" channel is not a good option for modification because its values are constantly being restored by BASIC.
It is not possible to create a new channel from BASIC. Another difficulty is that without Interface I, OPEN will only work with K, S, and P and so it is necessary to provide some other way of opening your own channels. This short assembler routine will create a new channel and associate a stream with it:
LD HL,(PROG) ; A new channel starts below PROG DEC HL ; LD BC,0x0005 ; Make space CALL 0x1655 ; INC HL ; HL points to 1st byte of new channel data LD A,0xfd ; LSB of output routine LD (HL),A ; INC HL ; PUSH HL ; Save address of 2nd byte of new channel data LD A,0xfd ; MSB of output routine LD (HL),A ; INC HL ; LD A,0xc4 ; LSB of input routine LD (HL),A ; INC HL ; LD A,0x15 ; MSB of input routine LD (HL),A ; INC HL ; LD A,0x55 ; Channel name 'U' LD (HL),A ; POP HL ; Get address of 2nd byte of output routine LD DE,(CHANS) ; Calculate the offset to the channel data AND A ; and store it in DE SBC HL,DE ; EX DE,HL ; LD HL,'STRMS' ; LD A,0x04 ; Stream to open, in this case #4. ADD A,0x03 ; Calculate the offset and store it in HL ADD A,A ; LD B,0x00 ; LD C,A ; ADD HL,BC ; LD (HL),E ; LSB of 2nd byte of new channel data INC HL ; LD (HL),D ; MSB of 2nd byte of new channel data RET
This routine will create a channel "U" (any ASCII character from 0 to 255 will be accepted by the standard ROM since it ignores this information). This channel has an output routine at 65021 and an input routine at 5572 (generates error report 'J'). A new entry is created in the STRMS table which points to the address of the second byte of new channel data. The stream number can be from -3 to 15, but it is best to stick to the range 4 to 15 and not modify the system streams (-3 to -1) or the standard streams (0 to 3).
This article was written by Dr. Sean A. Irvine and Dr. Ian Collier and later revised by Andrew Owen for the comp.sys.sinclair FAQ. The channel routine is a revised version of code by Ian Beardsmore, first published in Your Spectrum issue 7.