.........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 1 of 5, ZX Computing December 1986 Streams and channels might sound like computer jargon, but if you can master them you can control some powerful new facilities. [Note that the code in STREAMS.TAP "PART3", "PART4" and "PART5"] [includes the code from the previous part. JimG] Whenever we PRINT something, or INPUT something, we are making use of streams and channels. PRINT really means PRINT #2, although the #2 is often omitted. Similarly, INPUT really means INPUT #0, and LPRINT means PRINT #3. The number after the "#" symbol is called the STREAM NUMBER. Now, not every stream number is usable. If, after switching the Spectrum on, you type PRINT #6, you'll get an error report "O Invalid stream". This is because stream number 6 is not attached to a CHANNEL, so before I do anything else I'd better explain what a CHANNEL is. A CHANNEL is a device used for either inputting or outputting (or both) bytes of data. The TV screen is a channel, since characters may be printed onto it. Every channel has a name, consisting of a single letter of the alphabet. The screen is called channel "S" because "S" stands for Screen. An OPEN # statement is used to attach a channel to a stream, so if you type in the BASIC statement OPEN #6,"S" then from now on, whenever you use PRINT #6; the output will be printed to the screen. Similarly, the keyboard is a device used for inputting, therefore it too is a channel. It has a name - "K" for Keyboard. You can attach a stream to the keyboard just as easily as to the screen: OPEN #N,"K" will attach stream number N to the keyboard. You can have a maximum of sixteen streams altogether, and these are numbered from zero to fifteen. There is no such thing as stream number sixteen, and if you attempt to use it then you'll get an error message. Streams zero to three, however, are already spoken for. Stream zero and stream one are both attached to channel "K" (the keyboard), stream two is attached to channel "S" (the screen), and stream three is attached to channel "P" (the printer). Channels "S" and "P" can only be used for output, so PRINT #2; is allowed, but INPUT #2; is not. surprisingly though, channel "K" can be used either for inputting or for outputting. INPUT #0; is the same as a conventional INPUT statement, but PRINT# 0 will print onto the lower two lines of the screen (where INPUT text normally appears). If you follow a PRINT #0; statement with PAUSE 0, for example, then you'll see this effect in action. CHANNEL "P" Channel "P" is a weird one. On the 16K and 48K versions of the Spectrum, and also on the Spectrum 128 in 48K mode, channel "P" is the ZX Printer. On the Spectrum 128 in 128K mode, however, channel "P" is the built-in RS232 socket, into which you can plug a compatible printer, another computer, or anything else you feel like. Incidentally on the Spectrum 128 in 128K mode you can also INPUT from channel "P". This simply means that the computer can accept data from its RS232 port, which means that two computers can effectively talk to each other via this link. It is not possible to print to a ZX type printer from the 128 in 128K mode, at least not in BASIC, but that's where this series comes in. If you have connected a ZX Interface One then there will be other channels too. Channel "M" - the microdrives; channel "N" - the network; channels "T" and "B" - Interface One's RS232 port (as opposed to the Spectrum 128's RS232 port). This, then, concludes the list of standard channels available on the Spectrum, but the use of channels to do anything else is vastly under-rated. This four-part [Actually five-part. JimG] series is devoted to changing all that. It is the purpose of this series to explore strange new programming techniques, to seek out new channels and new information, to boldly split infinitives which no stream has split before. We shall invent, in machine code, new channels which can print to the screen in large or small letters; new channels which will allow the use of WINDOWS on the screen (a la QL); new channels which allow completely successful communication between Spectrums and QLs over the network; new channels which enable the ZX Printer to work on the Spectrum 128 in 128K mode; new channels which will allow users of the Spectrum 128 to make use of Read and Write files in RAM-disc. C.I.A. So how does it all work? Well that is the question I intend to answer in this article. It all hinges on an area of memory that you probably haven't used before, called the "Channel Information Area", and which happens to be situated directly below the BASIC program area. To create a channel you merely have to organise the memory in the channel information area in the correct way, and of course, provide the machine code to effect PRINT and INPUT to and from the channel. Here's how it works. Every channel must have a "Channel Information Block" stored in the channel information area. This is simply a chunk of memory dedicated to the channel, containing all of its system variables and the addresses of its input and output subroutines. The three primary channels "K", "S" and "P" all use only five bytes, but they are exceptions. All other channels require a minimum of eleven bytes, as we shall see. Incidentally, there is a fourth primary channel (that is, a channel available on the unexpanded standard Spectrum) called channel "R" but it is only available in machine code. We'll learn more of that later. The channel information blocks for these four primary channels look like this: Bytes 0 and 1: Address of PRINT # routine. Bytes 2 and 3: Address of INPUT # routine. Byte 4: Name of channel (ASCII character code). The Interface One channels all need a minimum of eleven bytes, and their channel information is addressed by the IX register. Their channel information blocks look like this: IX+00 (2 bytes): The address 0008. IX+02 (2 bytes): The address 0008. IX+04: Name of channel (ASCII character code). IX+05 (2 bytes): Address of PRINT # routine (in Shadow ROM). IX+07 (2 bytes): Address of INPUT # routine (in Shadow ROM). IX+09 (2 bytes): Length of channel information block (minimum 0008). IX+0B: Any additional information. ROM ROUTINES The standard PRINT and INPUT subroutines in the ROM (RST 10 and CALL 15E6,INPUT_AD) will "expect" a primary channel. Therefore, control will jump to the address given by bytes 0 and 1 (for RST 10), or to the address given by bytes 2 and 3 (for INPUT_AD). In the case of the Interface One channels, this address will in both cases be 0008. At this address the Shadow ROM is paged in and a routine in the Shadow ROM will redirect control to the address given by (IX+05/06) or (IX+07/08). Our channels will look different again. We too shall use IX to index the channel information, however our channels will look like this: IX+00 (2 bytes): Address of PRINT # routine. IX+02 (2 bytes): Address of INPUT # routine. IX+04: Name of channel (ASCII character code). IX+05 (2 bytes): The number 1234, which identifies this as a user defined channel. IX+07 (2 bytes): Address of CLOSE # routine. IX+09 (2 bytes): Length of channel information block (minimum 0008). IX+0B: Any additional information. STREAMS ARE IMPORTANT TOO You see, each stream has its own unique two-byte system variable. STRMS_00 is at 5C16, STRMS_01 is at 5C18, and so on. In addition there are three streams which are available in machine code but not in BASIC. These are stream FD, stream FE, and stream FF. They too have system variables. STRMS_FD is at 5C10, STRMS_FE is at 5C12, and STRMS_FF is at 5C14. Stream FD is permanently attached to channel "K" and should not be changed. Similarly, stream FE is permanently attached to channel "S". Stream FF is interesting. It is permanently attached to channel "R" - an internal machine code channel, which is capable of inserting bytes into the Spectrum's dynamic memory layout. We'll make use of this feature later on. The STRMS variables themselves link the stream to the channel. If the STRMS variable for any particular stream contains 0000 then it means that the stream in question is closed (ie. it has no channel attached to it). If the variable is non-zero then the stream is attached to a channel. In particular it is attached to the channel whose channel information block begins at address (CHANS)+(STRMS_n)-1. It follows, therefore, that it must surely be easier, from a machine code point of view, to OPEN a channel than to CLOSE it, since to open a channel all you have to do is to make room for the new channel information at the end of the CHANS area, and to assign the appropriate STRMS variable to attach the stream to. Closing such a channel, on the other hand, may be more difficult, because if the channel information block is not the last one in the CHANS area then all of the channel information blocks which follow it will have [to be moved] down in memory, which in turn means that the corresponding STRMS variable will have to be altered accordingly. Any or all of the STRMS variables may have to be altered if this is the case. CLOSE_A The main program accompanying this article is called CLOSE_A [But ignore it, as it's "riddled with bugs" according to the author (see next issue). JimG]. Its purpose is to close one of our new channels. On entry the A register must contain the stream number to be closed. If the stream is already closed, or if the channel is not one of our user-defined ones, then the subroutine will return, having done nothing. If, on the other hand, the stream specified is attached to one of our new channels then the routine will close the channel, reclaim memory used by that channel, and adjust any of the STRMS variables required. The carry flag must also be assigned on entry. If the carry flag is set it means that any data stored in buffers must be sent out, but if the carry flag is reset it means that any such data is to be ignored. Also included is a program CLEAR_CHANS which will close all such user defined channels. Any data stored in buffers will be lost if this routine is called. You may think it strange starting with a CLOSE # routine, whilst not having an OPEN # routine, but that's where the cliff-hanger comes in. Next month I'll start opening some new channels - in particular I'll deal with printing to the screen in large or small letters. Remember that once a channel is opened it may be used at will in BASIC. As soon as you have a channel which can print sixty- four characters across the screen then you can use a common or garden BASIC PRINT statement to print things to the channel. Study the programs I've listed. They are not relocatable, because they are designed to link in with all the other bits of program which you'll get in the next three episodes. If you still haven't properly understood the concepts of channels or streams then maybe next month you'll be a whole lot wiser, when I shall start giving you some concrete examples of the ideas in use. See you then. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Listing 1 B000 08 CLOSE_A EX AF,AF' ;Store the carry flag B001 CD2117 CALL STR_DATA_A ;HL points to STRMS info B004 78 LD A,B B005 B1 OR C B006 C8 RET Z ;Return if stream already closed B007 E5 PUSH HL ;Stack pointer to STRMS info B008 21E2A3 LD HL,#A3E2 B00B 09 ADD HL,BC B00C E1 POP HL ;HL points to STRMS info B00D D0 RET NC ;Return if channel is K, S, R or P B00E DD2A4F5C LD IX,(CHANS) B012 DD09 ADD IX,BC B014 DD2B DEC IX ;IX points to channel info B016 DD7E05 LD A,(IX+#05) B019 FE34 CP #34 B01B C0 RET NZ ;Return if not a user-defined channel B01C DD7E06 LD A,(IX+#06) B01F FE12 CP #12 B021 C0 RET NZ ;Return if not a user-defined channel B022 3600 LD (HL),#00 B024 23 INC HL B025 3600 LD (HL),#00 ;Load STRMS info with 0000 to ;signal "stream closed" B027 C5 PUSH BC ;Stack CHANS displacement B028 DD6E07 LD L,(IX+#07) B02B DD6608 LD H,(IX+#08) ;HL= address of CLOSE # routine B02E 08 EX AF,AF' B02F DC2C16 CALL C,CALL_JUMP ;Call subroutine if required B032 DDE5 PUSH IX B034 E1 POP HL ;HL= address of channel info B035 DD4E09 LD C,(IX+#09) B038 DD460A LD B,(IX+#0A) ;BC= length of channel info B03B C5 PUSH BC ;Stack length of channel info B03C CDE819 CALL RECLAIM_2 ;Reclaim memory used by channel B03F 3E10 LD A,#10 ;A= number of streams B041 21165C LD HL,#5C16 ;HL points to STRMS data for stream 0 B044 22745C C_LOOP LD (T_ADDR),HL ;Store STRMS pointer B047 5E LD E,(HL) B048 23 INC HL B049 56 LD D,(HL) ;DE= STRMS data for stream A B04A C1 POP BC ;BC= length of channel info reclaimed B04B E1 POP HL ;HL= CHANS disp. of channel closed B04C E5 PUSH HL B04D C5 PUSH BC B04E A7 AND A B04F ED52 SBC HL,DE B051 300B JR NC,C_CONT ;Jump if channel info for stream A ;has moved B053 EB EX DE,HL ;HL= STRMS data for stream A B054 A7 AND A B055 ED42 SBC HL,BC ;Reduce to allow for reclaimed area B057 EB EX DE,HL ;DE= modified STRMS data B058 2A745C LD HL,(T_ADDR) ;HL points to STRMS data B05B 73 LD (HL),E B05C 23 INC HL B05D 72 LD (HL),D ;Store modified data B05E 2A745C C_CONT LD HL,(T_ADDR) ;HL points to STRMS data B061 23 INC HL B062 23 INC HL ;HL points to next STRMS variable B063 3D DEC A B064 20DE JR NZ,C_LOOP ;Repeat for all sixteen streams B066 F1 POP AF B067 F1 POP AF ;Balance the stack B068 C9 RET - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Listing 2 B069 3E10 CLEARCHAN LD A,#10 ;A= number of streams B06B 3D CC_LOOP DEC A ;A= next stream number B06C F5 PUSH AF ;Stack stream number and Zero flag B06D A7 AND A ;Reset carry to signal ;"Do not send data" B06E CD00B0 CALL CLOSE_A ;Close stream A if it is a ;user-defined stream B071 F1 POP AF ;A= current stream number B072 20F7 JR NZ,CC_LOOP ;Repeat for all streams B074 C9 RET - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 2 of 5, ZX Computing January 1987 Toni Baker sheds light on windows for all Spectrums. In the last part we discussed what streams and channels were and what we could do with them. Also we introduced the concept of "user defined channels" - that is - new channels created in machine code to do various tasks not possible in any other way. Last month's episode included a program to CLOSE a user defined channel. Apologies are due here as unfortunately it was riddled with bugs, so I've listed a new version in this episode. You might like to play "Spot the bugs" by comparing last month's listing to this month's - see how many you can find! The included version works, however, as does the rest of the program. Windows This article consists of a new user defined channel called a WINDOW. The concept of a window is very simple. Users of the QL will already be familiar with them. A window is a rectangular region of the screen which may be treated as if it were a whole screen - if you print text to a window it will only appear within the confines of this rectangle, and once the window gets completely full it will scroll independently of the rest of the screen. It is also possible to clear a window, in any colour scheme, without clearing the rest of the screen. The program given will in fact cater for two different types of window, which I have called "Fast" and "Slow" windows (although in practice there seems to be no noticeable difference in speed between the two). A "Fast" window will do exactly what is described above, and no more. The standard character set is used (although this may be altered by changing the system variable (CHARS) at address 5C36) and all characters are eight pixels wide. A "Slow" window, however, has two important extras. The first is left-justification. Put simply this means that words are treated as whole chunks of characters and will not be split up - thus if a word is too long to fit at the end of one line then it will be printed as a whole at the start of the next line. This is achieved by cleverly storing all of the characters in a buffer until the end of the word is reached, and then deciding whether or not it will fit on the line. This means that when you use a Slow window channel you never have to worry about spacing the words out to fit on the line - the channel does that all by itself. The second improvement is that you are not restricted to eight bit wide characters - you can use seven bit wide; six bit wide; four bit wide even if you like. This means that you can fit more characters in each line than would normally be possible. When a window becomes filled then it will scroll. Normally it will pause at this point, but it is possible to open a window which has the scroll pause disabled - in such a case the window will scroll automatically each time without pausing to wait for you. While a window is paused the screen will appear to freeze. You may then press either SPACE (which continues with the scroll) or BREAK (which will break out giving report message "D BREAK - CONT repeats"). Even if the scroll pause is disabled it will still be possible to break out at the point of scrolling by pressing BREAK. The Spectrum, unlike the QL, does not come equipped with the statement CLS #N (which clears a window), so I have had to make special provision to ensure that a window may be cleared easily from BASIC. I have used CHR$ 0 as a clear-window control, thus to clear a window attached to stream four it would merely be necessary to use the command: PRINT #4;CHR$ 0; and the job will be done. You can put other print items on the same line at course. All of the normal controls are allowed, including AT and TAB, all of the colour controls (PAPER, INK, FLASH and BRIGHT) as well as INVERSE. OVER is not incorporated, however, when used with SLOW windows. OVER 1 will switch to printing with double-height characters, while OVER 0 will revert to printing with single- height characters. As was stated in the last article, all of our new user defined channels will be identified by the fact that IX+05 will contain the two-byte value 1234h, where IX points to the channel information block. Figure one lists the channel information used by a window channel. Note that if the window is a Fast window then W_SCROLLS (IX+15) will be the last variable - IX+16 and beyond will not exist. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 1 IX+00 W_OUT Address of WINDOW output routine (=B4F1) IX+02 W_IN Address of WINDOW output routine (=15C4, REPORT_J) IX+04 W_CHNAME Name of channel (="W") IX+05 W_IDEN New channel identifier (=1234) IX+07 W_CLOSE Address of empty buffer routine (=B4D6 for Slow windows or 0052 for Fast windows) IX+09 W_CHLEN Length of channel information block IX+0B W_FLAGS Various flags, defined as follows: Bit 7: Reset if leading space required, set otherwise Bit 6: Not used Bit 5: Set if scroll pause enabled, reset otherwise Bit 4: Set for Slow window, reset for Fast window Bit 3: Set for INVERSE 1, reset for INVERSE 0 Bit 2: Set if using double height, reset otherwise Bits 1,0: Number of control parameters required IX+0C W_XCOORD Current x coordinate of print position IX+0D W_WIDTH Number of characters per line IX+0E W_YCOORD Current y coordinate of print position IX+0F W_HEIGHT Height of window, in squares IX+10 W_PRPOS Address within screen of current print position IX+12 W_HOME Address within screen of top left-hand corner of window IX+14 W_ATTR Colours currently being used for window IX+15 W_SCROLLS Counts number of scrolls allowed before scroll pause, +1 IX+16 W_PIX Position within square of current print position IX+17 W_CH_WID Width of characters, in pixels IX+18 W_CHARS Address of character set (20-7F) minus 100h IX+1A W_UDG Address of character set (80-A4) IX+1C W_WIDTH_8 Width of window, in squares IX+1D W_LEN Number of characters stored in buffer IX+1E W_BUFFER The buffer itself - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - UDGs The program commences with a few routines suitable for all user defined channels, not just windows. At address B000 the routine CLOSE_NEW will close the new channel which is attached to stream A. At address B061 is a routine which will close all user defined channels, but any data stored in buffers will be lost if this routine is used. At address B06D the routine OPEN_NEW will open any new user defined channel and attach it to a stream. To use this sub-routine the registers must be pre-assigned with the required values, as specified in the notes above the routine. Then there are a couple of routines which will work with most user defined channels, though not necessarily all of them. CHR_TYPE (address B0B7) will expand keywords, and will count incoming control parameters. It will also set or reset the "Leading Space" bit (see Figure One) according to whether or not the character is a space. If, on return from this sub- routine the sign flag is set, it means that there is no more work to be done for this character. CHR_TYPE_2 (address B12F) will also deal with the comma control and the TAB function, and in a similar manner will also return with the sign flag set if work on the character is finished. And then we come to the program itself. The program will work both on 16K/48K Spectrums, and on the Spectrum 128, in either mode - but when the program is run on a Spectrum 128 in 128K mode then it will be possible to define a window either on screen zero (the normal screen) or screen one. Note that the Spectrum 128 has no built-in software which may print onto screen one, so this program will make up for one of the few flaws in the new machine. Users of the Spectrum 128 must ensure that the machine stack resides below address BFFF otherwise the program will crash. To open a window onto screen one it is only necessary to set bit 7 of the B register (the co-ordinate of the window) when opening the channel. Here's how you open a window channel. Firstly you must assign the registers as defined in the notes above OPEN_WINDOW (address B212), and then all you have to do is call the sub-routine OPEN_WINDOW itself. Voila, the job will be done, and you can use the channel in BASIC just as easy (if not easier) as you can in machine code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Listing 1 To close a new channel. On entry the A register must contain the stream number to be closed. If called from the label CLOSE_CLR then the Carry flag must be set if data in buffers is to be sent, or reset if such data is to be lost. B000 37 CLOSE_NEW SCF ;Signal "Data in buffer to be sent" B001 F5 CLOSE_CLR PUSH AF ;Stack stream number B002 08 EX AF,AF' ;F' stores the Carry flag B003 F1 POP AF ;A= stream number to close B004 CD2117 CALL STR_DATA_A ;BC= stream data for given stream; ;HL points to appropriate STRMS var B007 E5 PUSH HL ;Stack pointer to STRMS variable B008 21EBFF LD HL,#FFEB ;HL= -15h B00B 09 ADD HL,BC B00C E1 POP HL ;HL points to STRMS variable B00D D0 RET NC ;Return with channels "K", "S", "R" ;and "P", and also with streams which ;are already closed B00E DD2A4F5C LD IX,(CHANS) ;IX points to channel info area B012 DD09 ADD IX,BC B014 DD2B DEC IX ;IX points to channel information ;block for the given channel B016 DD7E05 LD A,(IX+#05) B019 FE34 CP #34 B01B C0 RET NZ B01C DD7E06 LD A,(IX+#06) B01F FE12 CP #12 B021 C0 RET NZ ;Return if this is not one of our new ;user-defined channels B022 3600 LD (HL),#00 B024 23 INC HL B025 3600 LD (HL),#00 ;Reset the STRMS variable, thus ;closing the stream B027 C5 PUSH BC ;Stack displacement into channel ;information area B028 DD6E07 LD L,(IX+#07) B02B DD6608 LD H,(IX+#08) ;HL= address of "Send data in buffer" ;subroutine B02E 08 EX AF,AF' ;Retrieve Carry flag B02F DC2C16 CALL C,CALL_JUMP ;Send data if required B032 DDE5 PUSH IX B034 E1 POP HL ;HL points to channel info block B035 DD4E09 LD C,(IX+#09) B038 DD460A LD B,(IX+#0A) ;BC= length of channel info block B03B C5 PUSH BC B03C CDE819 CALL RECLAIM_2 ;Reclaim the memory used by block B03F C1 POP BC B040 3E10 LD A,#10 ;A= number of streams to consider B042 21165C LD HL,STRMS_00 ;HL points to stream zero variable B045 5E CLOSELOOP LD E,(HL) B046 23 INC HL B047 56 LD D,(HL) ;DE= stream variable for next stream B048 E3 EX (SP),HL ;HL= stream variable for stream ;just closed B049 A7 AND A B04A ED52 SBC HL,DE B04C 19 ADD HL,DE B04D 300B JR NC,CLOSENEXT ;Jump unless the channel information ;block for this stream has moved B04F EB EX DE,HL B050 A7 AND A B051 ED42 SBC HL,BC B053 EB EX DE,HL ;DE= updated stream variable for ;this stream B054 E3 EX (SP),HL ;HL points to 2nd byte of STRMS var B055 2B DEC HL ;HL points to 1st byte of STRMS var B056 73 LD (HL),E B057 23 INC HL B058 72 LD (HL),D ;Store new value of streams variable B059 E3 EX (SP),HL B05A E3 CLOSENEXT EX (SP),HL ;HL points to 2nd byte of STRMS var B05B 23 INC HL ;HL points to next STRMS variable B05C 3D DEC A B05D 20E6 JR NZ,CLOSELOOP ;Loop back to consider remaining ;streams B05F F1 POP AF ;Balance the stack B060 C9 RET ;Return To clear all new channels. Any data held in buffers will be lost, and the memory used by all of the new user-defined channels will be reclaimed. B061 3E10 CLEAR_NEW LD A,#10 ;A= number of streams to consider B063 3D CLEARLOOP DEC A ;A= stream number of next stream B064 F5 PUSH AF B065 A7 AND A ;Signal "Data in buffers to be lost" B066 CD01B0 CALL CLOSE_CLR ;Clear the channel associated with ;this stream B069 F1 POP AF B06A 20F7 JR NZ,CLEARLOOP ;Loop back until all streams cleared B06C C9 RET ;Return To open a new channel. On entry A' must contain the designated stream number; A must contain the name of the channel (an ASCII character code); BC must contain the length of the required channel information block; DE must contain the input address; HL must contain the output address; and IX must contain the address of a subroutine to send any data stored in buffers (or 0052h if this does not apply). B06D C5 OPEN_NEW PUSH BC ;Stack length of channel info block B06E DDE5 PUSH IX ;Stack close buffer address B070 F5 PUSH AF ;Stack name of channel B071 D5 PUSH DE ;Stack input address B072 E5 PUSH HL ;Stack output address B073 C5 PUSH BC ;Stack length of info block (again) B074 08 EX AF,AF' ;A= stream number to attach B075 CD2117 CALL STR_DATA_A ;BC= stream data for given stream B078 78 LD A,B B079 B1 OR C ;[This next command was missed out of the listing in part 2.] ;[A correction wasn't printed until part 5. JimG] B07A C1 POP BC B07B 2802 JR Z,OPEN_NEW2 ;Jump unless stream already open B07D CF RST #08 ;"O Invalid stream" error report B07E 17 DEFB #17 B07F E5 OPEN_NEW2 PUSH HL ;Stack pointer to stream variable B080 2A535C LD HL,(PROG) B083 2B DEC HL ;HL points to 80h byte at end of ;channel information area B084 CD5516 CALL MAKE_ROOM ;Make room for new channel info B087 23 INC HL ;HL points to new channel info block B088 22515C LD (CURCHL),HL ;Make this the current channel B08B E5 PUSH HL B08C DDE1 POP IX ;IX points to channel info block B08E 23 INC HL ;HL points to second byte of info B08F ED4B4F5C LD BC,(CHANS) ;BC points to start of CHANS area B093 A7 AND A B094 ED42 SBC HL,BC ;HL= required stream value B096 EB EX DE,HL ;DE= required stream value B097 E1 POP HL ;HL points to stream variable B098 73 LD (HL),E B099 23 INC HL B09A 72 LD (HL),D ;Assign stream variable with required ;value, thus opening the stream B09B DDE5 PUSH IX B09D E1 POP HL ;HL points to channel info block B09E D1 POP DE ;DE= output address B09F C1 POP BC ;BC= input address B0A0 CDAEB0 CALL OPENSTORE ;Store these addresses in info block B0A3 F1 POP AF ;A= name of channel B0A4 77 LD (HL),A ;Store name of channel B0A5 23 INC HL B0A6 3634 LD (HL),#34 B0A8 23 INC HL B0A9 3612 LD (HL),#12 ;Signal "User-defined channel" B0AB 23 INC HL B0AC D1 POP DE ;DE= close buffer address B0AD C1 POP BC ;BC= length of channel info block B0AE 73 OPENSTORE LD (HL),E B0AF 23 INC HL B0B0 72 LD (HL),D ;Store output or close address B0B1 23 INC HL B0B2 71 LD (HL),C B0B3 23 INC HL B0B4 70 LD (HL),B ;Store input address or block length B0B5 23 INC HL B0B6 C9 RET ;Return To deal with parameters of control codes, and keyword tokens. This subroutine may be used with any user-defined channel provided that (IX+0B) contains flags. Bits 1 and 0 count incoming parameters for use with colour controls, AT and TAB; while bit 7 is set if a space has just been printed to the channel. On return the Sign flag will be set if no more needs to be done, otherwise the Zero flag will be set if the character is a graphics character, and the Carry flag will be set for block graphics and control codes. B0B7 4F CHR_TYPE LD C,A ;C= character to "print" B0B8 DD7E0B LD A,(IX+#0B) ;A contains various flags B0BB 57 LD D,A ;D contains various flags B0BC E603 AND #03 ;A= number of parameters expected B0BE 2816 JR Z,CTYP_GO ;Jump if no parameters expected B0C0 15 DEC D B0C1 7A LD A,D B0C2 DD770B LD (IX+#0B),A ;Decrement parameter count B0C5 E603 AND #03 B0C7 2805 JR Z,CTYP_LAST ;Jump if this is the last parameter B0C9 FD71D5 LD (TVDATA)hi,C ;Store this (middle) parameter B0CC 185E JR CTYP_DONE ;Jump to exit B0CE 3A0E5C CTYP_LAST LD A,(TVDATA)lo ;A= original control code B0D1 FD46D5 LD B,(TVDATA)hi ;B= middle parameter (if one exists) B0D4 1847 JR CTYP_CTRX ;Jump to exit B0D6 213B5C CTYP_GO LD HL,FLAGS ;HL points to system flags B0D9 1EA3 LD E,"SPECTRUM" ;E= the 1st Spectrum 128K keyword B0DB CB66 BIT 4,(HL) B0DD 2002 JR NZ,CTYP_MODE ;Jump if in 128K mode B0DF 1EA5 LD E,"RND" ;E= the 1st Spectrum 16K/48K keyword B0E1 79 CTYP_MODE LD A,C ;A= character to print B0E2 BB CP E B0E3 380F JR C,CTYP_NTOK ;Jump unless this is a keyword B0E5 DD7E0B LD A,(IX+#0B) ;A contains various flags B0E8 07 RLCA ;Bit 0 = leading space bit B0E9 AE XOR (HL) B0EA E601 AND #01 B0EC AE XOR (HL) B0ED 77 LD (HL),A ;Assign leading space bit as reqd B0EE 79 LD A,C ;A= character to print B0EF CD520B CALL P_T&UDG ;Expand this keyword B0F2 1838 JR CTYP_DONE ;Jump to exit B0F4 FE80 CTYP_NTOK CP #80 B0F6 380D JR C,CTYP_NGRA ;Jump unless this is a graphics char B0F8 DDCB0BBE RES 7,(IX+#0B) ;Signal "Leading space will be reqd" B0FC FE90 CP #90 B0FE 3003 JR NC,CTYP_UDG ;Jump if this is a UDG B100 BF CP A ;Set the Zero flag B101 37 SCF ;Set the Carry flag B102 C9 RET ;Return B103 BF CTYP_UDG CP A ;Set Zero flag; reset Carry flag B104 C9 RET ;Return B105 FE20 CTYP_NGRA CP #20 B107 380C JR C,CTYP_CTRL ;Jump with control characters B109 DDCB0BBE RES 7,(IX+#0B) ;Signal "Leading space will be reqd" B10D 2004 JR NZ,CTYP_SPCE ;Jump unless character is SPACE B10F DDCB0BFE SET 7,(IX+#0B) ;Signal "Leading space not reqd" B113 A7 CTYP_SPCE AND A ;Reset Zero flag; reset Carry flag B114 C9 RET ;Return B115 FE10 CTYP_CTRL CP #10 B117 3804 JR C,CTYP_CTRX ;Jump to exit with codes 00 to 0F B119 FE18 CP #18 B11B 3803 JR C,CTYP_PARA ;Jump with codes 10 to 18 B11D FEFF CTYP_CTRX CP #FF ;Reset Zero flag; set Carry flag B11F C9 RET ;Return B120 320E5C CTYP_PARA LD (TVDATA)lo,A ;Store control code B123 14 INC D ;Signal "One parameter required" B124 FE16 CP #16 B126 3801 JR C,CTYP_SET ;Jump with codes 10 to 16 B128 14 INC D ;Signal "Two parameters required" B129 DD720B CTYP_SET LD (IX+#0B),D ;Store number of reqd parameters B12C F6FF CTYP_DONE OR #FF ;Set the Sign flag B12E C9 RET ;Return To deal with the comma control, and the TAB function as well. This subroutine may be used with any user-defined channel, provided that (IX+0B) contains flags as above; that (IX+0C) contains the x coordinate (or column number) of the current print position; and that (IX+0D) contains the width of the line (or total number of columns allowed). Flags on return are as above, except that the Sign flag will be set if comma or TAB have been dealt with. B12F CDB7B0 CHR_TYPE2 CALL CHR_TYPE ;Deal with ctrl params and keywords B132 DD5E0D CHRTYPE2A LD E,(IX+#0D) ;E= width of line B135 F5 PUSH AF ;Stack the flags B136 FE06 CP #06 B138 2806 JR Z,CTYP_COMM ;Jump with comma control B13A FE17 CP #17 B13C 2810 JR Z,CTYP_TAB ;Jump with TAB control B13E F1 POP AF ;Restore the flags B13F C9 RET ;Return B140 DD7E0C CTYP_COMM LD A,(IX+#0C) ;A= column number of print position B143 3C INC A B144 E6F8 AND #F8 B146 C608 ADD A,#08 ;A= column number of next field B148 BB CP E B149 3811 JR C,CTYP_SPCS ;Jump if column number is in range B14B 7B LD A,E B14C 180E JR CTYP_SPCS ;Else tab past end of current line B14E 61 CTYP_TAB LD H,C B14F 68 LD L,B ;HL= TAB parameter B150 1600 LD D,#00 ;DE= width of line B152 ED52 CTYP_LOOP SBC HL,DE B154 30FC JR NC,CTYP_LOOP B156 19 ADD HL,DE ;Reduce modulo line length B157 7D LD A,L ;A= column number to TAB to B158 A7 AND A B159 2001 JR NZ,CTYP_SPCS ;Jump unless column zero is required B15B 7B LD A,E ;Tab past end of current line B15C DDBE0C CTYP_SPCS CP (IX+#0C) B15F 2807 JR Z,CTYP_EXIT ;Jump if required column reached B161 F5 PUSH AF B162 CD390C CALL PO_SPACE ;Print a space B165 F1 POP AF B166 18F4 JR CTYP_SPCS ;Jump back to see if finished B168 F1 CTYP_EXIT POP AF ;A= control just dealt with B169 F6FF OR #FF ;Set the Sign flag B16B C9 RET ;Return The following two entry points PAGE_0 and PAGE_7 will page in RAM page zero and RAM page seven respectively. The routines will have no effect unless called from a Spectrum 128 in 128K mode. B16C C5 PAGE_0 PUSH BC B16D 0600 LD B,#00 ;B= required page number B16F 1803 JR PAGE_B ;Jump forward B171 C5 PAGE_7 PUSH BC B172 0607 LD B,#07 ;B= required page number B174 F5 PAGE_B PUSH AF B175 FDCB0166 BIT 4,(FLAGS) B179 280E JR Z,PAGE_EXIT ;Jump if not in 128K mode B17B 3A5C5B LD A,(BANK_M) ;A= current page flags B17E E6F8 AND #F8 B180 B0 OR B B181 01FD7F LD BC,#7FFD ;BC= port no reqd to change page B184 325C5B LD (BANK_M),A ;Signal required RAM page B187 ED79 OUT (C),A ;Actually change page B189 F1 PAGE_EXIT POP AF B18A C1 POP BC B18B C9 RET ;Return The following three subroutines require that initially HL points to a byte on the screen, and will each adjust the value of HL in a different way. The first will point HL to the pixel immediately below the given one; the second will point HL down one line (ie. down eight pixels); and the third will find the address of the corresponding attribute byte. B18C 24 DOWN_1 INC H ;Assume within a character square B18D 7C LD A,H B18E E607 AND #07 B190 C0 RET NZ ;Return if this is so B191 7D LD A,L B192 C620 ADD A,#20 B194 6F LD L,A ;Assume crossing screen thirds B195 D8 RET C ;Return if this is so B196 7C LD A,H B197 D608 SUB #08 B199 67 LD H,A ;Adjust for within screen third B19A C9 RET ;Return B19B 7D DOWN_8 LD A,L B19C C620 ADD A,#20 B19E 6F LD L,A ;Assume within screen third B19F D0 RET NC ;Return if this is so B1A0 7C LD A,H B1A1 C608 ADD A,#08 B1A3 67 LD H,A ;Adjust for crossing screen thirds B1A4 C9 RET ;Return B1A5 7C ATTR_ADDR LD A,H B1A6 1F RRA B1A7 1F RRA B1A8 E606 AND #06 ;Isolate screen third number B1AA F6B0 OR #B0 B1AC CB14 RL H ;Carry flag = screen number B1AE 1F RRA B1AF 67 LD H,A ;HL= attribute byte address B1B0 C9 RET ;Return The remainder of the program in this article is concerned exclusively with WINDOW channels. The following subroutine will move the print position to the start of the (A+1)th line of the current window. B1B1 DDBE0F LINE_A CP (IX+W_HEIGHT) B1B4 D29F1E JP NC,REPORT_B ;Give error if line number too big B1B7 DD360C00 LD (IX+W_XCOORD),#00 ;Reset x coordinate B1BB DD770E LD (IX+W_YCOORD),A ;Assign new y coordinate B1BE DD6E12 LD L,(IX+W_HOME)lo B1C1 DD6613 LD H,(IX+W_HOME)hi ;HL= address of top LH corner B1C4 A7 AND A B1C5 2806 JR Z,LINEFOUND ;Jump if line zero required B1C7 47 LD B,A ;B= line number required B1C8 CD9BB1 LINE_LOOP CALL DOWN_8 ;HL= address of next line B1CB 10FB DJNZ LINE_LOOP ;HL= address of required line B1CD CD9AB3 LINEFOUND CALL STOREADDR ;Store this address as print position B1D0 DDCB0BFE SET 7,(IX+W_FLAGS);Signal "Leading space not required" B1D4 DDCB0B66 BIT 4,(IX+W_FLAGS) B1D8 C8 RET Z ;Return if this is a "Fast" channel B1D9 DD361600 LD (IX+W_PIX),#00;Reset print pos within char square B1DD C9 RET ;Return The next subroutine will clear one line of a window, given that A contains the attribute byte with which to clear; that HL contains the address of the line within the screen; and that BC contains the width of the line in squares, less one. B1DE C5 CLW_LINE PUSH BC ;Stack length of line, less one B1DF E5 PUSH HL ;Stack address of line B1E0 F5 PUSH AF ;Stack attribute byte B1E1 CDA5B1 CALL ATTR_ADDR ;HL= address of attribute line B1E4 F1 POP AF ;A= attribute byte B1E5 54 LD D,H B1E6 5D LD E,L B1E7 13 INC DE ;DE points to second attribute byte B1E8 77 LD (HL),A ;Store first attribute byte B1E9 EDB0 LDIR ;Store remaining attribute bytes B1EB E1 POP HL ;HL= address of line B1EC C1 POP BC ;BC= length of line, less one B1ED 3E08 LD A,#08 ;A= number of rows per line B1EF 54 LD D,H B1F0 5D LD E,L B1F1 13 INC DE ;DE points to second byte in line B1F2 E5 CLWL_LOOP PUSH HL ;Stack address of 1st byte in row B1F3 D5 PUSH DE ;Stack address of 2nd byte in row B1F4 C5 PUSH BC ;Stack length of line, less one B1F5 3600 LD (HL),#00 ;Reset first byte B1F7 EDB0 LDIR ;Reset remaining bytes B1F9 C1 POP BC ;BC= length of line, less one B1FA D1 POP DE ;DE= address of 2nd byte in row B1FB E1 POP HL ;HL= address of 1st byte in row B1FC F5 PUSH AF ;Stack loop counter B1FD CD8CB1 CALL DOWN_1 ;HL= addr of 1st byte in next row B200 F1 POP AF ;A= loop counter B201 14 INC D ;DE= address of 2nd byte in row B202 3D DEC A B203 20ED JR NZ,CLWL_LOOP ;Repeat for all eight rows B205 C9 RET ;Return This very short subroutine will fetch the width of the current window, measured in squares, into the C register. B206 DD4E0D GET_WIDTH LD C,(IX+W_WIDTH);C= width of window, in characters B209 DDCB0B66 BIT 4,(IX+W_FLAGS) B20D C8 RET Z ;Return with "Fast" windows B20E DD4E1C LD C,(IX+W_WIDTH_8) ;C= width of window, in squares B211 C9 RET To open a WINDOW channel and attach it to a stream. This subroutine will open either a "Fast" or a "Slow" window channel. The registers must be assigned on entry as follows: For both types of channel: A' The stream number to which the channel is to be attached A The attribute byte used initially by the window B The number of lines between the top of the window and the top of the screen C The number of squares between the left of the window and the left of the screen D The height of the window, in squares E The width of the window, in squares H 00 if the scroll pause is disabled; FF if the scroll pause is enabled L 00 for a "Fast" window; FF for a "Slow" window For SLOW windows only: BC' Address of normal character set, minus 100h DE' Address of graphics character set (beginning at CHR$ 80h) H' The width of these characters, in pixels B212 F5 OPEN_WIND PUSH AF ;Stack attribute byte B213 C5 PUSH BC ;Stack coordinates of window B214 D5 PUSH DE ;Stack size of window B215 E5 PUSH HL ;Stack flags B216 011600 LD BC,#0016 ;BC= length of "Fast" chan info block B219 DD215200 LD IX,#0052 ;Signal "No buffer", presuming ;"Fast" channel B21D 2C INC L ;[The next line was printed as 2021 in the magazine,] ;[which jumped to the wrong address. JimG] B21E 2020 JR NZ,OPWN_CREA ;Jump with "Fast" channels B220 1600 LD D,#00 ;DE= window width, in squares B222 62 LD H,D B223 6B LD L,E ;HL= window width, in squares B224 29 ADD HL,HL B225 29 ADD HL,HL B226 29 ADD HL,HL ;HL= window width, in pixels B227 D9 EXX B228 7C LD A,H ;A= character width in pixels B229 D9 EXX B22A 5F LD E,A ;E= character width in pixels B22B 3EFF LD A,#FF ;A= -1 B22D 3C OPWN_LOOP INC A B22E ED52 SBC HL,DE B230 30FB JR NC,OPWN_LOOP ;A= width of window, in characters B232 D9 EXX B233 6F LD L,A B234 D9 EXX ;L'= width of window, in characters B235 5F LD E,A ;DE= width of window, in characters B236 211E00 LD HL,#001E ;HL= length of "Slow" channel info ;block, excluding buffer B239 19 ADD HL,DE ;HL= total length of chan info block B23A 44 LD B,H B23B 4D LD C,L ;BC= total length of chan info block B23C DD21D6B4 LD IX,EMPTY_2 ;IX points to empty buffer subrt B240 21F1B4 OPWN_CREA LD HL,WINDOW ;HL points to output subroutine B243 11C415 LD DE,REPORT_J ;DE points to input error routine B246 3E57 LD A,"W" ;A= name of this channel B248 CD6DB0 CALL OPEN_NEW ;Open the channel B24B E1 POP HL ;HL= flags B24C D1 POP DE ;DE= size of window B24D C1 POP BC ;BC= coordinates of window B24E F1 POP AF ;A= attribute byte for window B24F DD7714 LD (IX+W_ATTR),A ;Store attribute byte B252 AF XOR A ;A= 00 B253 CB1D RR L B255 1F RRA ;[The next line was printed as CB17 RR H in the magazine,] ;[but CB17 is RL A, and CB1C is RR H. As this is setting ] ;[the "Scroll pause" and "Fast/Slow" flags in bits 5 & 4 ] ;[of W_FLAGS, CB1C RR H must be correct. JimG ] B256 CB1C RR H B258 1F RRA B259 1F RRA B25A 1F RRA ;Construct flags byte B25B DD770B LD (IX+W_FLAGS),A;Store these flags B25E DD730D LD (IX+W_WIDTH),E;Store window width B261 DD720F LD (IX+W_HEIGHT),D ;Store window height B264 78 LD A,B ;A= y coordinate of window B265 E698 AND #98 B267 F640 OR #40 B269 DD7713 LD (IX+W_HOME)hi,A ;Store high part of address of top ;left-hand corner of window B26C 78 LD A,B ;A= y coordinate of window B26D 0F RRCA B26E 0F RRCA B26F 0F RRCA ;[There was a chunk of the ENTER subroutine at B2BB from the next issue] ;[printed in the magazine at this point - which I've omitted. JimG] ;[The next line was printed as E6B0 AND E0 in the magazine,] ;[but E6B0 is AND B0, and E6E0 is AND E0. The latter command] ;[gives the correct result. JimG] B270 E6E0 AND #E0 B272 B1 OR C B273 DD7712 LD (IX+W_HOME)lo,A ;Store low part of address of top ;left-hand corner of window B276 DDCB0B66 BIT 4,(IX+W_FLAGS) B27A 2816 JR Z,CLSWINDOW ;Jump with "Fast" window B27C DD731C LD (IX+W_WIDTH_8),E ;Store window width, in squares B27F D9 EXX B280 DD750D LD (IX+W_WIDTH),L;Store window width, in characters B283 DD7417 LD (IX+W_CH_WID),H ;Store character width, in pixels B286 DD7318 LD (IX+W_CHARS)lo,E B289 DD7219 LD (IX+W_CHARS)hi,D ;Store addr of character set -100h B28C DD711A LD (IX+W_UDG)lo,C B28F DD701B LD (IX+W_UDG)hi,B;Store addr of graphics char set Control does not return immediately from the above routine, but continues into the next subroutine, whose purpose it is to clear a window and move the print position to the top left-hand corner. It is the WINDOW equivalent to CLS. B292 AF CLSWINDOW XOR A ;A= 00 B293 CDB1B1 CALL LINE_A ;Move print pos to top left corner B296 CD06B2 CALL GET_WIDTH ;C= width of window, in squares B299 2804 JR Z,CLSW_CONT ;Jump with "Fast" windows B29B DD361D00 LD (IX+W_LEN),#00;Clear buffer, abandoning contents B29F DD460F CLSW_CONT LD B,(IX+W_HEIGHT) ;B= height of window, in squares B2A2 DD7E14 LD A,(IX+W_ATTR) ;A= attribute byte B2A5 DD361501 LD (IX+W_SCROLLS),#01 ;Reset scroll count B2A9 CD71B1 CALL PAGE_7 ;Use RAM page 7 in case scr 1 in use B2AC 0D DEC C ;C= width of window, less one B2AD C5 CLSW_LOOP PUSH BC B2AE 0600 LD B,#00 ;BC= width of window, less one B2B0 F5 PUSH AF B2B1 CDDEB1 CALL CLW_LINE ;Clear next line of window B2B4 F1 POP AF B2B5 C1 POP BC B2B6 10F5 DJNZ CLSW_LOOP ;Clear whole window B2B8 C36CB1 JP PAGE_0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The Windows program will continue next month .........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 3 of 5, ZX Computing February 1987 The concluding part of Toni Baker's Windows program. [Note that the code in STREAMS.TAP "PART3"] [includes the code from "PART2". JimG] Last month we began experimenting with windows. The listing continues ... As a demonstration, the extra program WIND_DEMO which I've tagged on at the end at address B544 will open a window twenty-four squares wide by eight squares high, positioned AT 2,1 (relative to the whole screen) with yellow paper and blue ink. Furthermore, the window will be a SLOW window, so words won't ever be cut in half, and although the standard character set is used, they are defined to be seven bits wide, not eight, so you get more characters than you would normally. Running the program once will open the window and attach it to stream four. Thereafter PRINT #4 will print onto the window. Follow through For those of you who wish to follow the program through and understand how it all works, I'll tell you that the program starts running from location WINDOW (address B4F1) with the A register containing the character to be printed, whenever RST 10 is used with this channel. Oh - incidentally - while we're talking about the WINDOW routine, take a look at the four instructions following the label WIND_CTRL. The CALL instruction carries out the control code function, the POP instruction restores the control character to the A register, and the RET instruction terminates everything - the routine has finished - control will then pass back to the RST 10 sub-routine itself, and then back to the PRINT statement which caused the RST 10 to be used. But ... what's this AND A instruction doing just before the RET? The comment beside the instruction reads "Reset the carry flag". Why? - Surely everything's finished now. We shouldn't need to worry about flags should we? Unfortunately we do. You see when a control code such as PAPER 4 or INVERSE 1 is used in a PRINT statement then the appropriate control codes are sent to RST 10 to be carried out. The PRINT routine expects the carry flag to be reset on return from such a routine, and will produce the error message "C Nonsense in BASIC" if this is not the case. Next month I'll give you no less than two new channels: a modified network channel for owners of the ZX Interface 1 which will successfully communicate with a QL, and a channel which will allow users of the Spectrum 128 to use the standard ZX Printer whilst in 128K mode, saving a lot of money in the process. See you then. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The following subroutine prints a newline; ie. it moves the print position to the left-hand edge of the next line down. B2BB CDC3B2 ENTER CALL ENTER_1 ;Print a single newline once B2BE DDCB0B56 BIT 2,(IX+W_FLAGS) B2C2 C8 RET Z ;Return if using single height B2C3 DD7E0E ENTER_1 LD A,(IX+W_YCOORD) ;A= current y coordinate B2C6 3C INC A ;A= new y coordinate B2C7 DDBE0F CP (IX+W_HEIGHT) B2CA C2B1B1 JP NZ,LINE_A ;Jump if in range to move print pos B2CD DDCB0B6E BIT 5,(IX+W_FLAGS) B2D1 2812 JR Z,SCROLL ;Jump if scroll pause disabled B2D3 DD3515 DEC (IX+W_SCROLLS);Decrement scroll count B2D6 200D JR NZ,SCROLL ;Jump unless scroll pause required B2D8 DD7E0F LD A,(IX+W_HEIGHT) B2DB DD7715 LD (IX+W_SCROLLS),A ;Re-initialise scroll count B2DE 3E7F SCR_PAUSE LD A,#7F B2E0 DBFE IN A,(#FE) ;Scan part of the keyboard B2E2 1F RRA B2E3 38F9 JR C,SCR_PAUSE ;Pause until SPACE pressed B2E5 CD541F SCROLL CALL BREAK_KEY B2E8 D2000D JP NC,REPORT_D ;Give error report if BREAK pressed B2EB DD7E0E LD A,(IX+W_YCOORD) ;A= y coordinate of bottom line B2EE F5 PUSH AF B2EF CDB1B1 CALL LINE_A ;Move print pos to start of bottom ;line of window B2F2 C1 POP BC ;B= number of lines to copy B2F3 CD06B2 CALL GET_WIDTH ;C= width of window in squares B2F6 DD6E12 LD L,(IX+W_HOME)lo B2F9 DD6613 LD H,(IX+W_HOME)hi ;HL= address of top left corner B2FC CD71B1 CALL PAGE_7 ;Use RAM page 7 in case scr 1 used B2FF C5 SCR_LOOP1 PUSH BC ;Stack loop counter B300 0600 LD B,#00 ;BC= width of line B302 C5 PUSH BC B303 E5 PUSH HL B304 CDA5B1 CALL ATTR_ADDR ;HL= addr of this attribute line B307 112000 LD DE,#0020 B30A EB EX DE,HL ;DE= addr of this attribute line B30B 19 ADD HL,DE ;HL= addr of next attribute line B30C EDB0 LDIR ;Copy one attribute line B30E E1 POP HL B30F C1 POP BC B310 E5 PUSH HL B311 CD9BB1 CALL DOWN_8 ;HL= address of next line B314 D1 POP DE ;DE= address of this line B315 E5 PUSH HL B316 3E08 LD A,#08 ;A= number of rows per line B318 C5 SCR_LOOP2 PUSH BC B319 D5 PUSH DE B31A E5 PUSH HL B31B EDB0 LDIR ;Copy one row from line B31D E1 POP HL B31E D1 POP DE B31F C1 POP BC B320 24 INC H ;HL= addr of next row of next line B321 14 INC D ;DE= addr of next row of this line B322 3D DEC A B323 20F3 JR NZ,SCR_LOOP2 ;Copy whole line B325 E1 POP HL B326 C1 POP BC B327 10D6 DJNZ SCR_LOOP1 ;Transfer all required lines B329 0B DEC BC ;BC= length of line, less one B32A DD7E14 LD A,(IX+W_ATTR) ;A= attribute byte B32D CDDEB1 CALL CLW_LINE ;Clear bottom line B330 C36CB1 JP PAGE_0 ;Restore RAM page zero and return The following subroutine deals with all control codes except for comma-control and TAB (these are dealt with by CHR_TYPE2 at address B12F). On entry the A register will contain the control code itself, while any parameters required will be stored in B (middle parameter, if one exists) and C (last parameter). B333 FE0D CONTROLS CP "enter" B335 2884 JR Z,ENTER ;Jump to deal with "enter" B337 D610 SUB #10 B339 D8 RET C ;Return with codes 00 to 0F B33A FE06 CP #06 B33C 2827 JR Z,CTRL_AT ;Jump to deal with AT B33E D0 RET NC ;Return with codes 17 to 1F B33F C65F ADD A,CTRL_INFO lo B341 6F LD L,A B342 26B3 LD H,CTRL_INFO hi;HL points to control info table B344 46 LD B,(HL) ;B= bit mask for this control B345 111400 LD DE,#0014 ;DE= IX displacement to W_ATTR B348 FE63 CP #63 B34A 3802 JR C,CTRL_CONT ;Jump unless ctrl is INVERSE/OVER B34C 1E0B LD E,#0B ;DE= IX displacement to W_FLAGS B34E DDE5 CTRL_CONT PUSH IX B350 E1 POP HL ;HL points to channel info block B351 19 ADD HL,DE ;HL points to variable to alter B352 79 LD A,C ;A= control parameter B353 48 LD C,B ;C= bit mask B354 0F RRCA B355 07 CTRL_LOOP RLCA B356 CB18 RR B B358 30FB JR NC,CTRL_LOOP ;A= ctrl parameter in correct posn B35A AE XOR (HL) B35B A1 AND C B35C AE XOR (HL) ;Mix in reqd bits according to mask B35D 77 LD (HL),A ;Store variable B35E C9 RET B35F 07 CTRL_INFO DEFB #07 ;Bit mask for INK B360 38 DEFB #38 ;Bit mask for PAPER 80 DEFB #80 ;Bit mask for FLASH B362 40 DEFB #40 ;Bit mask for BRIGHT B363 08 DEFB #08 ;Bit mask for INVERSE B364 04 DEFB #04 ;Bit mask for OVER The next subroutine is the WINDOW version of the AT function. It performs the function AT B,C for the current window. B365 C5 CTRL_AT PUSH BC B366 78 LD A,B ;A= proposed y coordinate B367 CDB1B1 CALL LINE_A ;Move print pos to start of this line B36A C1 POP BC B36B 79 LD A,C ;A= proposed x coordinate B36C A7 AND A B36D C8 RET Z ;Return if task already done B36E DDBE0D CP (IX+W_WIDTH) B371 D29F1E JP NC,REPORT_B ;Give error report if out of range B374 DD770C LD (IX+W_XCOORD),A ;Store new x coordinate B377 0600 LD B,#00 ;BC= x coordinate B379 DDCB0B66 BIT 4,(IX+W_FLAGS) B37D 281A JR Z,AT_EXIT ;Jump with "Fast" channels B37F E5 PUSH HL ;Stack address of start of line B380 DD4E17 LD C,(IX+W_CH_WID) ;BC= character width in pixels B383 60 LD H,B B384 68 LD L,B ;HL= 0000 B385 09 AT_LOOP_1 ADD HL,BC B386 3D DEC A B387 20FC JR NZ,AT_LOOP_1 ;HL= no of pixels to start of char B389 0603 LD B,#03 B38B CB3C AT_LOOP_2 SRL H B38D CB1D RR L B38F 1F RRA B390 10F9 DJNZ AT_LOOP_2 ;HL= no of squares to start of char B392 07 RLCA B393 07 RLCA B394 07 RLCA ;A= pixel position within char square B395 DD7716 LD (IX+W_PIX),A ;Store in variable B398 C1 POP BC ;BC= address of start of line B399 09 AT_EXIT ADD HL,BC ;HL= new print position address B39A DD7510 STOREADDR LD (IX+W_PRPOS)lo,L B39D DD7411 LD (IX+W_PRPOS)hi,H ;Store new print position B3A0 C9 RET ;Return The following subroutine is very short and simple. It merely sets the attribute byte corresponding to the screen address in HL. B3A1 E5 SET_ATTR PUSH HL B3A2 CDA5B1 CALL ATTR_ADDR ;HL= address of attribute byte B3A5 DD7E14 LD A,(IX+W_ATTR) ;A= current colours B3A8 77 LD (HL),A ;Store attribute byte B3A9 E1 POP HL B3AA C9 RET ;Return This subroutine is intended for use with "Slow" windows only. It will plot one row of a character onto the screen. B3AB CDA1B3 PLOT_ROW CALL SET_ATTR ;Set attribute byte B3AE 7E LD A,(HL) ;A= byte from screen B3AF AA XOR D B3B0 A0 AND B B3B1 AA XOR D ;Mix in bits from character B3B2 77 LD (HL),A ;Store in screen B3B3 79 LD A,C ;A= low byte of mask B3B4 3C INC A B3B5 280A JR Z,PLRW_EXIT ;Exit if all bits stored on screen B3B7 23 INC HL ;HL points to next screen byte B3B8 CDA1B3 CALL SET_ATTR ;Set this attribute byte as well B3BB 7E LD A,(HL) ;A= byte from screen B3BC AB XOR E B3BD A1 AND C B3BE AB XOR E ;Mix in bits from character B3BF 77 LD (HL),A ;Store in screen B3C0 2B DEC HL ;HL points to original screen byte B3C1 C38CB1 PLRW_EXIT JP DOWN_1 ;Point HL one pixel down, and return This subroutine will calculate 8*A+DE, and will also collect the current print position. B3C4 6F PREPARE LD L,A B3C5 2600 LD H,#00 ;HL= character code B3C7 29 ADD HL,HL B3C8 29 ADD HL,HL B3C9 29 ADD HL,HL ;HL= eight times character code B3CA 19 ADD HL,DE B3CB EB EX DE,HL ;DE= address of pixel expansion B3CC DD6E10 LD L,(IX+W_PRPOS)lo B3CF DD6611 LD H,(IX+W_PRPOS)hi ;HL= address of print position B3D2 C9 RET ;Return This subroutine will collect one byte from the pixel expansion pointed to by DE and will invert it if necessary. B3D3 1A GET_ROW LD A,(DE) ;A= next row of pixel expansion B3D4 13 INC DE ;DE points to next row B3D5 DDCB0B5E BIT 3,(IX+W_FLAGS) B3D9 C8 RET Z ;Return unless INVERSE 1 in operation B3DA 2F CPL ;Otherwise invert the row B3DB C9 RET This next and very important subroutine will actually print a character, specified in the A register, onto the current window. B3DC F5 PRINT_CHR PUSH AF ;Stack character to print B3DD F5 PUSH AF ;Stack character to print (again) B3DE DD7E0C LD A,(IX+W_XCOORD) ;A= current X coordinate B3E1 DDBE0D CP (IX+W_WIDTH) B3E4 CCBBB2 CALL Z,ENTER ;Print newline if at end of line B3E7 DDCB0B66 BIT 4,(IX+W_FLAGS) B3EB 2034 JR NZ,PCHRSLOW ;Jump with "Slow" window B3ED F1 POP AF ;A= character to print B3EE ED5B365C LD DE,(CHARS) ;DE= addr of normal char set - 100h B3F2 2012 JR NZ,PCHR_OK_1 ;Jump with ASCII character B3F4 300A JR NC,PCHR_UDG ;Jump with user-defined graphics B3F6 47 LD B,A B3F7 CD380B CALL PO_GR_1 ;Construct block graphic B3FA 11925C LD DE,MEMBOT ;DE points to pixel expansion B3FD AF XOR A B3FE 1806 JR PCHR_OK_1 ;Jump forward B400 D690 PCHR_UDG SUB #90 B402 ED5B7B5C LD DE,(UDG) ;DE points to user-defined graphics B406 CDC4B3 PCHR_OK_1 CALL PREPARE ;DE= address of pixel expansion ;HL= address of print position B409 CDA1B3 CALL SET_ATTR ;Set attribute for this square B40C E5 PUSH HL B40D 0608 LD B,#08 ;B= number of rows per line B40F CDD3B3 PCHRLOOP1 CALL GET_ROW ;A= next row from expansion B412 CD71B1 CALL PAGE_7 ;Use RAM page 7 in case scrn 1 in use B415 77 LD (HL),A ;Store row in screen B416 24 INC H ;HL points to next row B417 CD6CB1 CALL PAGE_0 ;Restore RAM page zero B41A 10F3 DJNZ PCHRLOOP1 ;Print whole character B41C E1 POP HL B41D 23 INC HL ;HL= new print position B41E C3B2B4 JP PCHR_EXIT ;Jump to exit B421 DDCB0B56 PCHRSLOW BIT 2,(IX+W_FLAGS) B425 281E JR Z,PCHRSLOW2 ;Jump unless using double height B427 DD7E0F LD A,(IX+W_HEIGHT) ;A= height of window B42A 3D DEC A ;A= y coordinate of bottom line B42B DDBE0E CP (IX+W_YCOORD) B42E 2015 JR NZ,PCHRSLOW2 ;Jump unless at bottom line B430 DD4E0C LD C,(IX+W_XCOORD) ;C= current x coordinate B433 47 LD B,A ;B= current y coordinate B434 05 DEC B ;B= y coordinate after scroll B435 DD7E0B LD A,(IX+W_FLAGS) B438 F5 PUSH AF ;Stack flags B439 C5 PUSH BC ;Stack coordinates B43A CDC3B2 CALL ENTER_1 ;Scroll the screen once B43D C1 POP BC ;BC= coordinates B43E CD65B3 CALL CTRL_AT ;Move print position back where it ;belongs B441 F1 POP AF B442 DD770B LD (IX+W_FLAGS),A ;Restore the flags B445 F1 PCHRSLOW2 POP AF ;A= character to print B446 DD5E18 LD E,(IX+W_CHARS)lo B449 DD5619 LD D,(IX+W_CHARS)hi;DE= addr of normal chr set - 100h B44C 2008 JR NZ,PCHR_OK_2 ;Jump with ASCII characters B44E D680 SUB #80 B450 DD5E1A LD E,(IX+W_UDG)lo B453 DD561B LD D,(IX+W_UDG)hi;DE points to graphics chr set B456 CDC4B3 PCHR_OK_2 CALL PREPARE ;DE= address of pixel expansion ;HL= address of print position B459 E5 PUSH HL ;Stack address of print position B45A 01FFFF LD BC,#FFFF B45D DD7E17 LD A,(IX+W_CH_WID) ;A= width of chr in pixels B460 CB38 PCHRMASK1 SRL B B462 CB19 RR C B464 3D DEC A B465 20F9 JR NZ,PCHRMASK1 ;BC= mask, not yet in position B467 DD7E16 LD A,(IX+W_PIX) ;A= pixel posn within chr square B46A A7 AND A B46B 2808 JR Z,PCHRMASK3 ;Jump if mask OK B46D 37 PCHRMASK2 SCF B46E CB18 RR B B470 CB19 RR C B472 3D DEC A B473 20F8 JR NZ,PCHRMASK2 ;Otherwise rotate mask into place B475 3E08 PCHRMASK3 LD A,#08 ;A= number of rows per line B477 F5 PCHRLOOP2 PUSH AF ;Stack loop counter B478 CDD3B3 CALL GET_ROW ;A= next row from expansion B47B D5 PUSH DE ;Stack pointer into expansion B47C 57 LD D,A B47D DD7E16 LD A,(IX+W_PIX) ;A= pixel posn within chr square B480 A7 AND A B481 2807 JR Z,PCHR_ROW ;Jump if pixels correctly aligned B483 CB1A PCHRSHIFT RR D B485 CB1B RR E B487 3D DEC A B488 20F9 JR NZ,PCHRSHIFT ;Shift pixels into position B48A CD71B1 PCHR_ROW CALL PAGE_7 ;Set RAM page 7 in case scrn 1 in use B48D CDABB3 CALL PLOT_ROW ;Plot row onto screen B490 DDCB0B56 BIT 2,(IX+W_FLAGS) B494 C4ABB3 CALL NZ,PLOT_ROW ;And again if using double height B497 CD6CB1 CALL PAGE_0 ;Restore RAM page zero B49A D1 POP DE ;DE points to pixel expansion B49B F1 POP AF ;A= loop counter B49C 3D DEC A B49D 20D8 JR NZ,PCHRLOOP2 ;Print whole character B49F E1 POP HL ;HL= original print position B4A0 DD7E16 LD A,(IX+W_PIX) ;A= original position within square B4A3 DD8617 ADD A,(IX+W__CH_WID) ;Allow for width of character B4A6 FE08 PCHR_POS CP #08 B4A8 3805 JR C,PCHR_POS2 ;Jump if print position OK B4AA D608 SUB #08 B4AC 23 INC HL ;Otherwise amend it B4AD 18F7 JR PCHR_POS ;Loop back to try again B4AF DD7716 PCHR_POS2 LD (IX+W_PIX),A ;Store new position within square B4B2 CD9AB3 PCHR_EXIT CALL STOREADDR ;Store new print position B4B5 DD340C INC (IX+W_XCOORD) ;Increment x coordinate B4B8 F1 POP AF ;A= character just printed B4B9 C9 RET ;Return The following subroutine will empty the buffer ("Slow" windows only), printing the former contents onto the window. B4BA D4BBB2 EMPTY CALL NC,ENTER ;Print a newline if required B4BD DDE5 PUSH IX B4BF E1 POP HL ;HL points to channel info block B4C0 011D00 LD BC,#001D B4C3 09 ADD HL,BC ;HL points to variable W_LEN B4C4 7E LD A,(HL) ;A= number of characters in buffer B4C5 A7 AND A B4C6 C8 EMPTYLOOP RET Z ;Return if finished B4C7 23 INC HL ;HL points to next char in buffer B4C8 7E LD A,(HL) B4C9 E5 PUSH HL B4CA CDF4B0 CALL CTYP_NTOK ;Flags indicate type of character B4CD CDDCB3 CALL PRINT_CHR ;Print the character B4D0 E1 POP HL B4D1 DD351D DEC (IX+W_LEN) B4D4 18F0 JR EMPTYLOOP The following subroutine will empty the buffer ("Slow") or do nothin ("Fast"). It will decide whether or not a newline is required, and print one if so. B4D6 DDCB0B66 EMPTY_2 BIT 4,(IX+W_FLAGS) B4DA C8 RET Z ;Return with "Fast" windows B4DB 57 LD D,A ;D= next character to print B4DC DD7E1D LD A,(IX+W_LEN) ;A= length of word in buffer B4DF DD860C ADD A,(IX+W_XCOORD) ;A= potential x coordinate after ;the buffer has been emptied B4E2 DDBE0D CP (IX+W_WIDTH) B4E5 2002 JR NZ,EMPTY_OUT ;Jump unless word exactly fills line B4E7 53 LD D,E ;D= alternative char to print B4E8 37 SCF ;Signal "Newline not needed" B4E9 D5 EMPTY_OUT PUSH DE B4EA C5 PUSH BC B4EB CDBAB4 CALL EMPTY ;Empty buffer with newline if needed B4EE C1 POP BC B4EF F1 POP AF ;A= next character to print B4F0 C9 RET ;Return And now at last we have the window output subroutine itself. B4F1 DD2A515C WINDOW LD IX,(CURCHL) ;IX points to channel info block B4F5 CDB7B0 CALL CHR_TYPE ;Deal with keywords, etc. B4F8 F8 RET M ;Return if tasks completed B4F9 F5 PUSH AF ;Stack character to print B4FA 2824 JR Z,WIND_ABLE ;Jump with graphics characters B4FC 3013 JR NC,WINDASCII ;Jump with ASCII characters B4FE A7 AND A B4FF CC92B2 CALL Z,CLSWINDOW ;Clear window for CHR$ 0 B502 F1 POP AF B503 F5 PUSH AF B504 5F LD E,A ;Signal "Char not to be changed" B505 CDD6B4 CALL EMPTY_2 ;Empty buffer B508 CD32B1 CALL CHRTYPE2A ;Deal with TAB and comma control B50B CD33B3 WIND_CTRL CALL CONTROLS ;Deal with remaining ctrl characters B50E F1 POP AF B50F A7 AND A ;Reset Carry flag B510 C9 RET ;Return B511 FE20 WINDASCII CP "space" B513 200B JR NZ,WIND_ABLE ;Jump with all chars except "space" B515 1E0D LD E,"enter" ;Use "enter" as alternative char B517 CDD6B4 CALL EMPTY_2 ;Empty buffer B51A FE0D CP "enter" B51C 28ED JR Z,WIND_CTRL ;Jump if newline now required B51E 1806 JR WINDPRINT ;Jump to print "space" on window B520 DDCB0B66 WIND_ABLE BIT 4,(IX+W_FLAGS) B524 2004 JR NZ,#B52A ;Jump with "Slow" windows B526 F1 WINDPRINT POP AF B527 C3DCB3 JP PRINT_CHR ;Jump to print the character B52A 2A515C WIND_SLOW LD HL,(CURCHL) ;HL points to channel info block B52D 011D00 LD BC,#001D B530 09 ADD HL,BC ;HL points to variable W_LEN B531 7E LD A,(HL) ;A= number of chars in buffer B532 DDBE0D CP (IX+W_WIDTH) B535 2005 JR NZ,WINDSLOW2 ;Jump unless buffer full B537 E5 PUSH HL B538 CDBAB4 CALL EMPTY ;Empty buffer B53B E1 POP HL B53C F1 WINDSLOW2 POP AF B53D 34 INC (HL) ;Increment length of buffer B53E 4E LD C,(HL) B53F 0600 LD B,#00 ;BC= new length of buffer B541 09 ADD HL,BC B542 77 LD (HL),A ;Store char in buffer B543 C9 RET ;Return ;[The next subroutine gets overwritten by the code in Part 4. JimG] And finally - just a quick demonstration program to open a window and attach it to stream four. If you RUN this you'll be able to use PRINT #4; to print onto the window. B544 3E04 WIND_DEMO LD A,#04 B546 08 EX AF,AF' ;A'= stream number B547 2A7B5C LD HL,(UDG) ;HL= address of user-defined graphics B54A 0180FF LD BC,#FF80 B54D 09 ADD HL,BC B54E 44 LD B,H B54F 4D LD C,L ;BC'= address of UDGs - 80h B550 ED5B365C LD DE,(CHARS) ;DE'= addr of normal char set - 100h B554 2607 LD H,#07 ;H'= width of characters B556 D9 EXX B557 3E31 LD A,#31 ;A= attribute byte for window B559 010102 LD BC,#0201 ;BC= position of window B55C 111808 LD DE,#0818 ;DE= size of window B55F 21FFFF LD HL,#FFFF ;HL signals "Slow window" and ;"Scroll pause enabled" B562 CD12B2 CALL OPEN_WIND ;Open the window channel B565 215827 LD HL,#2758 B568 D9 EXX ;HL'= value required by BASIC B569 C9 RET ;Return - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 4 of 5, ZX Computing March 1987 If you thought that you couldn't use the ZX Printer with your new 128 then Toni Baker can prove you wrong. [Note that the code in STREAMS.TAP "PART4"] [includes the code from "PART3". JimG] This article contains not one, but two new channels which you can use on your Spectrum. The first is a channel which enables users of the Spectrum 128 (or the 128+2) to be able to use the ZX Printer - or some other compatible printer such as an Alphacom - even in 128K mode. If you don't have a Spectrum 128 then you don't need this new channel of course, because you can use the ZX Printer anyway. The second is not really a new channel at all, but a modification to an existing channel. It enables the Spectrum to be able to communicate freely with a QL via the Local Area Network available from the ZX Interface One. Of course communication between these computers via the network is already possible, and David Nowotnik has recently been doing an excellent series in ZX Computing on that very task. It is not my intention to duplicate any of his material, but merely to remove a couple of deficiencies in the channel itself. We'll return to this later, meanwhile back to the ZX Printer. Channel Z The program as such begins at address B544. If this seems a rather arbitrary address to you then I should explain that the reason it starts at B544 is that all the code follows on from the stuff in last month's article (which ran from B000 to B543). Indeed, some of the subroutines developed last month will be used in the routines given here. The trick to using the ZX Printer on the 128 is to avoid any possibility of erasing or corrupting the OLD printer buffer (addresses 5B00 to 5BFF) - this is because the memory in this range is used by the 128 to store various paging subroutines, system variables, and an alternative machine stack for use when accessing the vast banks of paged memory. If we can arrange things so that we can avoid corrupting this memory then the good old ZX Printer can still work normally. You see, all of the ZX Printer software in the ROM was written before the advent of the 128 and is not compatible with the requirements of the new machine. All we really need to do then is to create a NEW printer buffer somewhere in memory, and use the new buffer instead of the old one. The best possible place for this buffer would be as part of the channel information block for the channel - that would mean that the area occupied by the new printer buffer would be reclaimed whenever you closed the channel. We shall call this new channel "Z", for ZX-Printer. Take a look at Figure One - it shows the structure of the channel information block for the new channel. As you can see, the first eleven bytes store completely standard information, using the standard which was developed throughout this series. (IX+05/06) contains the constant 1234h, which identifies this as being a user-defined channel. This means that it may be opened or closed using some of the software listed last month (IX+0B), (IX+0C) and (IX+0D) also store information in the same format as the "W" channel given last month - this is so that we can exploit more of last month's subroutines. As you shall see, a collection of new channels is far more advantageous than a single new channel. Finally, the new printer buffer itself runs from (IX+0E) upwards, and is 100h bytes in length (same as the old printer buffer). The variable Z_XCOORD (IX+0C) will be zero if the printer buffer is completely empty, or non-zero otherwise. If the new buffer is full it will contain 20h. Z_WIDTH (IX+0D) remains constant at 20h - the width of the buffer. This is so that the TAB and comma-control routines from last month may be exploited successfully. The program begins at the label Z_CLOSE, which is the routine to perform all of the peripheral tasks necessary to close the channel. In fact this merely consists of testing whether or not the buffer is empty, and printing a newline if it isn't. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 1 IX+00 (2 bytes) Z_PRINT Address of channel "Z" output IX+02 (2 bytes) REPORT_J Address of channel "Z" input IX+04 (1 byte) Z_NAME Name of channel (ie. "Z") IX+05 (2 bytes) Z_IDEN Constant 1234h identifies new channel IX+07 (2 bytes) Z_CLOSE Address of close buffer routine IX+09 (2 bytes) Z_LEN Length of channel info (ie. 010E) IX+0B (1 byte) Z_FLAGS Various flags Bit 7: Set if leading space not reqd. for keywords, reset otherwise Bit 6: Not used Bit 5: Not used Bit 4: Not used Bit 3: Set if INVERSE status is ON, reset otherwise Bit 2: Set if OVER status is ON, reset otherwise Bit 1: Set if exactly two additional parameters reqd., reset otherwise Bit 0: Set if exactly one additional parameter reqd., reset otherwise IX+0C (1 byte) Z_XCOORD X coordinate of print position in buffer IX+0D (1 byte) Z_WIDTH Width of buffer (ie. 20h) IX+0E (100h bytes) Z_BUFFER New printer buffer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Newline The Z_NEWLINE routine may be compared to the ROM routine COPY_BUFF at address 0ECD. Its purpose is to transfer the contents of the buffer to the ZX Printer itself, before finally erasing the previous buffer contents. The routine Z_EMPTY does the actual erasing. You will notice a couple of differences between the ROM routine COPY_BUFF and my routine Z_NEWLINE. Firstly, the new printer buffer is used instead of the old one, and secondly the subroutine Z_COPY_LINE is called instead of the original COPY_LINE. In fact Z_COPY_LINE is itself very similar to the ROM's COPY_LINE routine (at 0EFA). The only difference in evidence is the effect of pressing the BREAK key - in my routine Z_EMPTY is called instead of CLEAR_PRB. This removes any possible chance of corrupting the old printer buffer area. A general purpose subroutine is included next - SEARCH_CH_ALL. It is entered with the A register containing the name of a channel. The routine will search through the channel information area looking for a channel with this name. If it finds one it will return with IX pointing to the channel information block and the carry reset, otherwise the carry will be set. We shall make use of this subroutine later. Z_PRINT Z_PRINT is the routine which "prints" a character held in the A register onto the new printer buffer. Note that the subroutine at address B12F comes from last month's article and will sort out all keywords, control parameters, and will deal with both the comma-control and the TAB function. On return from this subroutine the C register will contain any INVERSE or OVER parameters, or the x coordinate of any AT parameters. The Z_PRINT routine itself is really quite simple, bearing in mind that it has to deal with ASCII characters, block graphics, and UDGs, as well as OVER, INVERSE, AT and ENTER. Follow it through to see how all the various cases are dealt with. Finally (for this channel) we have Z_OPEN, which opens the channel and attaches it to a stream. On entry, the A register must contain the stream number to which the channel is to be attached and the subroutine will do the rest. It makes use of another subroutine from last article - the OPEN_NEW routine which will open user defined channels. Channel Q When communicating with the QL via the normal network channel you will notice a couple of problems. The first is that on the Spectrum the code for "enter" is 0D (thirteen), whereas on the QL the code for "enter" is 0A (ten). This means that, for instance, PRINT A$ on the QL cannot be matched by INPUT A$ on the Spectrum, nor vice versa. It is normally necessary to use PRINT A$;CHR$(13); on the QL in order that the Spectrum may use INPUT A$. Conversely, INPUT A$ on the QL needs to be matched by PRINT A$;CHR$ 10; on the Spectrum. What I intend to do is to modify the "N" channel attached to a particular stream so that the deficiencies are removed. In a modified "N" channel these problems disappear. PRINT A$ on the Spectrum may be matched by INPUT A$ on the QL, and vice versa. Other surprising advantages turn up; if you LIST a Spectrum program over a modified network channel then the QL will be able to LOAD the program with the single command LOAD NET1_2 (assuming that the Spectrum is station number 2) - or to transfer the program directly onto a QL microdrive ready for loading later with the single QL command COPY NET1_2 TO MDV1_PROG. The second problem is a little more complex, but not much so. The Spectrum command INPUT A$ actually expects to receive a string expression, not the contents of an actual string (although this is not the case with INPUT LINE). Fortunately for us, we seldom notice this because the Spectrum provides the surrounding quotes - this converts the string text into a string expression and everything is hunky dory - with one exception. If the string printed over the network by the QL contains a quotes character then the Spectrum will be unable to evaluate the result as a string expression and will halt with error "C Nonsense in BASIC". The cure to this problem is very simple. If, during Spectrum INPUT (as opposed to INKEY$ or INPUT LINE) a quotes character is received from a modified network channel THEN IT MUST BE CONVERTED TO TWO CONSECUTIVE QUOTES CHARACTERS in order that the expression will still evaluate. Let's take a look at the subroutines now. We begin with SHADOW_DE which will call a subroutine in the Shadow ROM whose address is DE. It achieves this by fooling the Spectrum into thinking that we are returning to the Shadow ROM from a Spectrum subroutine, and then simply paging out the Shadow ROM on completion. Q_PRINT is the routine which "prints" a character over the modified network. As you can see it is extremely simple, merely testing for an "enter" character and converting such to 0A, before using the original Shadow-PRINT subroutine to print the character. Q_INPUT is more complicated, though not much so. Firstly, the routine tests the error return address to see whether the character input is due to an INPUT command, or due to an INKEY$ function. If it turns out to be merely INKEY$ then things are just as simple as for Q_PRINT. The shadow input routine is called to read a character, and if that character is 0A then it is converted to "enter". If, however, we are dealing with INPUT (as opposed to INKEY$) then the first thing we must do is abandon altogether use of the EDITOR routine in the ROM (which we do by emptying the machine stack as far as the return address from the EDITOR call) and then supplying a new routine to replace EDITOR. We can call Q_INKEY$ as a sub- routine to fetch a single character from the network (converting 0A to 0D) and the flags will tell us if the character is valid or not. Then we can call a ROM subroutine ADD_CHAR_1 to actually insert the character into the input line. Quote characters must be inserted twice, not once (except during INPUT LINE). Finally, if an "enter" character is received then we may return, as if from the EDITOR routine, with the finished string expression in the INPUT area of RAM. MODIFY_N is the routine which will convert a "N" channel to do as it's told. On entry the A register must contain the stream number of an existing "N" channel which must have been opened normally. In such a channel (IX+00/01) and (IX+02/03) will normally contain the value 0008 which, if called, will page in the Shadow ROM and then call the actual output or input subroutine addressed by (IX+05/06) and (IX+07/08) respectively. The MODIFY_N subroutine will merely convert these 0008s to the addresses of Q_PRINT and Q_INPUT, leaving the addresses of the Shadow routines unchanged. The Shadow routines will then only be called when required by Q_PRINT or by Q_INPUT. Finally, a five byte demonstration of the "Z" channel. Simply calling Z_DEMO from BASIC will open a new channel "Z", and attach it to stream number four. Channel R In the next and final part of this series we shall examine the use of the existing channel "R", which may only ever be used in machine code. We shall also be creating a new channel, also called "R" (R for RAM-disc) which will enable you to create READ and WRITE files in the Spectrum 128's RAM-disc area, just as you can on the microdrives. Till then, keep smiling, and don't take anything seriously. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - B544 DD7E0C Z_CLOSE LD A,(Z_XCOORD) ;A= x coordinate within buffer B547 A7 AND A B548 C8 RET Z ;Return immediately if buffer empty B549 DDE5 Z_NEWLINE PUSH IX B54B E1 POP HL ;HL= address of channel info B54C 010E00 LD BC,#000E B54F 09 ADD HL,BC ;HL points to new printer buffer B550 F3 DI ;Disable interrupts B551 0608 LD B,#08 B553 C5 Z_NL_LOOP PUSH BC B554 CD73B5 CALL ZCOPYLINE ;Output next row to ZX Printer B557 C1 POP BC B558 10F9 DJNZ Z_NL_LOOP ;Output all eight rows B55A 3E04 LD A,#04 B55C D3FB OUT (#FB),A ;Switch off printer motor B55E FB EI ;Enable interrupts B55F DDE5 Z_EMPTY PUSH IX B561 E1 POP HL ;HL= address of channel info B562 010E00 LD BC,#000E B565 09 ADD HL,BC ;HL points to new printer buffer B566 AF XOR A ;A= 00 B567 77 ZEMPTLOOP LD (HL),A ;Zero next byte of buffer B568 23 INC HL ;HL points to next byte in buffer B569 10FC DJNZ ZEMPTLOOP ;Zero entire buffer B56B DDCB0BFE SET 7,(Z_FLAGS) ;Signal "Leading space not required" B56F DD770C LD (Z_XCOORD),A ;Reset x coordinate in buffer B572 C9 RET ;Return B573 78 ZCOPYLINE LD A,B ;A= row number to output B574 FE03 CP #03 B576 9F SBC A,A ;A= FF (last 2 rows); 00 (otherwise) B577 E602 AND #02 ;A= 02 (last 2 rows); 00 (otherwise) B579 D3FB OUT (#FB),A ;Switch on printer motor, but with ;slow speed for last two rows B57B 57 LD D,A ;D= "last 2 rows" flag B57C CD541F Z_CL_LOOP CALL BREAK_KEY ;Test BREAK key B57F 380A JR C,Z_CL_NEXT ;Jump forward unless BREAK pressed B581 3E04 LD A,#04 B583 D3FB OUT (#FB),A ;Switch off printer motor B585 FB EI ;Enable interrupts B586 CD5FB5 CALL Z_EMPTY ;Empty the new printer buffer B589 CF RST #08 ;Report "D, BREAK - CONT repeats" B58A 0C DEFB #0C B58B DBFE Z_CL_NEXT IN A,(#FE) ;A= status of printer B58D 87 ADD A,A B58E F8 RET M ;Return if printer not connected B58F 30EB JR NC,Z_CL_LOOP ;Wait until printer stylus is ready B591 C3120F JP COPY_L_2A Jump to print next row from buffer B594 DD2A4F5C SEARCHALL LD IX,(CHANS) ;IX points to base of chan info area B598 011400 LD BC,#0014 B59B 57 LD D,A ;D= name of channel to search for B59C DD09 SCHCHLOOP ADD IX,BC ;IX points to next chan info area B59E DD7E00 SEARCH_CH LD A,(IX+#00) B5A1 FE80 CP #80 B5A3 37 SCF B5A4 C8 RET Z ;Return with Carry set if channel ;not found B5A5 DD7E04 LD A,(IX+#04) ;A= name of channel pointed to B5A8 BA CP D B5A9 C8 RET Z ;Return with IX pointing to channel ;information area, and Carry reset, ;if search successful B5AA DD4E09 SEARCHNXT LD C,(IX+#09) B5AD DD460A LD B,(IX+#0A) ;BC= length of channel info B5B0 18EA JR SCHCHLOOP ;Loop back to continue search B5B2 DD2A515C Z_PRINT LD IX,(CURCHL) ;IX points to channel info area B5B6 CD2FB1 CALL CHR_TYPE2 ;Deal with keywords, control params, ;TAB and comma control B5B9 F8 RET M ;Return if tasks complete B5BA F5 PUSH AF B5BB 283B JR Z,ZGRAPHICS ;Jump with graphics characters B5BD 3033 JR NC,Z_ASCII ;Jump with ASCII characters B5BF FE0D CP #0D B5C1 2005 JR NZ,Z_PRCTRLS ;Jump unless char is "enter" B5C3 CD49B5 CALL Z_NEWLINE ;Print a newline B5C6 187F JR Z_EXIT ;and jump to exit B5C8 D614 Z_PRCTRLS SUB #14 B5CA 387B JR C,Z_EXIT ;Jump with controls 00 to 13 B5CC 0608 LD B,#08 ;B has bit 3 set B5CE 2805 JR Z,Z_INVOVER ;Jump with "INVERSE control" B5D0 3D DEC A B5D1 0604 LD B,#04 ;B has bit 2 set B5D3 200F JR NZ,Z_AT ;Jump unless char is "OVER control" B5D5 CB19 Z_INVOVER RR C B5D7 9F SBC A,A ;A= 00 (INVERSE 0 or OVER 0) ;or FF (INVERSE 1 or OVER 1) B5D8 DDAE0B XOR (Z_FLAGS) ;A= flags byte, but complemented if ;parameter is one B5DB A0 AND B ;A all bits reset, except that bit ;3 (INVERSE) or bit 2 (OVER) will be ;taken from flags byte, and ;complemented if parameter is one B5DC DDAE0B XOR (Z_FLAGS) B5DF DD770B LD (Z_FLAGS),A ;Bit 3 (INVERSE) or bit 2 (OVER) of ;flags byte will be assigned with ;parameter B5E2 1863 JR Z_EXIT ;Jump to exit B5E4 3D Z_AT DEC A B5E5 2060 JR NZ,Z_EXIT ;Jump unless control is "AT control" B5E7 79 LD A,C ;A= required x coordinate B5E8 FE20 CP #20 B5EA D29F1E JP NC,REPORT_B ;Error if out of range B5ED DD770C LD (Z_XCOORD),A ;Assign x coordinate as required B5F0 1855 JR Z_EXIT ;Jump to exit B5F2 ED5B365C Z_ASCII LD DE,(CHARS) ;DE= addr of character set - 100h B5F6 1811 JR Z_CHR_1 ;Jump forward B5F8 D690 ZGRAPHICS SUB #90 B5FA 3009 JR NC,Z_UDG ;Jump with user-defined graphics B5FC 47 LD B,A B5FD CD380B CALL PO_GR_1 ;Construct graphic in MEMBOT area B600 21925C LD HL,MEMBOT ;HL points to graphic form B603 180B JR Z_CHR_2 ;Jump forward B605 ED5B7B5C Z_UDG LD DE,(UDG) ;DE= address of user-defined graphics B609 6F Z_CHR_1 LD L,A B60A 2600 LD H,#00 B60C 29 ADD HL,HL B60D 29 ADD HL,HL B60E 29 ADD HL,HL B60F 19 ADD HL,DE ;HL points to required graphic form B610 DD7E0C Z_CHR_2 LD A,(Z_XCOORD) ;A= current x coordinate in buffer B613 FE20 CP #20 B615 E5 PUSH HL B616 CC49B5 CALL Z,Z_NEWLINE ;Print a newline if buffer full B619 D1 POP DE ;DE points to graphic form B61A DD7E0C LD A,(Z_XCOORD) ;A= current x coordinate in buffer B61D 3C INC A ;A= new x coordinate B61E DD770C LD (Z_XCOORD),A ;Store in system variable B621 C60D ADD A,#0D B623 4F LD C,A B624 0600 LD B,#00 ;BC= displacement to posn in buffer B626 DDE5 PUSH IX B628 E1 POP HL ;HL points to channel information B629 09 ADD HL,BC ;HL points to current posn in buffer B62A 0608 LD B,#08 B62C EB Z_CHRLOOP EX DE,HL ;DE= current buffer position ;HL points to graphic form B62D 1A LD A,(DE) ;A= byte from buffer B62E DDCB0B56 BIT 2,(Z_FLAGS) B632 2001 JR NZ,Z_CHROVER ;Jump if OVER off B634 AF XOR A B635 DDCB0B5E Z_CHROVER BIT 3,(Z_FLAGS) B639 2801 JR Z,Z_CHR_INV ;Jump if INVERSE on B63B 2F CPL B63C AE Z_CHR_INV XOR (HL) ;A= byte from graphic form, with ;OVER and INVERSE taken into account B63D 12 LD (DE),A ;Store in buffer B63E E5 PUSH HL ;Stack pointer into graphic form B63F 212000 LD HL,#0020 B642 19 ADD HL,DE ;HL points to appropriate byte in ;buffer for next row of character B643 D1 POP DE ;DE points into graphic form B644 13 INC DE ;DE points to next byte to use B645 10E5 DJNZ Z_CHRLOOP ;Print whole character into buffer B647 F1 Z_EXIT POP AF ;A= character just printed B648 A7 AND A ;Reset the Carry flag B649 C9 RET ;Return B64A 08 Z_OPEN EX AF,AF' ;A'= stream number to attach channel B64B 3E5A LD A,"Z" B64D CD94B5 CALL SEARCHALL ;Search for another "Z" channel B650 D2C415 JP NC,REPORT_J ;Error if one exists B653 DD2144B5 LD IX,Z_CLOSE ;IX= close buffer address B657 21B2B5 LD HL,Z_PRINT ;HL= output address B65A 11C415 LD DE,REPORT_J ;DE= input address B65D 010E01 LD BC,#010E ;BC= length of channel info B660 CD6DB0 CALL OPEN_NEW ;Open the channel B663 DD360B00 LD (Z_FLAGS),#00 ;Reset the flags B667 DD360D20 LD (Z_WIDTH),#20 ;Specify buffer width = 20h chars B66B C35FB5 JP Z_EMPTY ;Empty the buffer, and return B66E 210007 SHADOW_DE LD HL,UNPAGE B671 E5 PUSH HL ;Stack UNPAGE address in Shadow ROM B672 D5 PUSH DE ;Stack Shadow subroutine address B673 65 LD H,L ;HL= 0000 B674 E5 PUSH HL ;Stack 0000 signalling "Return to ;Shadow ROM address" B675 C30800 JP #0008 ;Jump to call Shadow subroutine B678 DD2A515C Q_PRINT LD IX,(CURCHL) ;IX points to channel information B67C F5 PUSH AF ;Stack character to print B67D FE0D CP "enter" B67F 2002 JR NZ,Q_PRINT_2 ;Jump unless character is "enter" B681 3E0A LD A,#0A ;A= QL's code for "enter" B683 DD5E05 Q_PRINT_2 LD E,(IX+#05) B686 DD5606 LD D,(IX+#06) ;DE= Shadow output address B689 CD6EB6 CALL SHADOW_DE ;Call network output in Shadow ROM B68C F1 POP AF ;A= character just printed B68D A7 AND A ;Reset the Carry flag B68E C9 RET ;Return B68F DD2A515C Q_INPUT LD IX,(CURCHL) ;IX points to channel information B693 2A3D5C LD HL,(ERR_SP) ;HL points to error return address B696 5E LD E,(HL) B697 23 INC HL B698 56 LD D,(HL) ;DE= error return address B699 217F10 LD HL,ED_ERROR B69C A7 AND A B69D ED52 SBC HL,DE B69F 2814 JR Z,Q_INPUT_2 ;Jump if in an INPUT channel B6A1 DD5E07 Q_INKEY$ LD E,(IX+#07) B6A4 DD5608 LD D,(IX+#08) ;DE= Shadow input address B6A7 CD6EB6 CALL SHADOW_DE ;Call network input from Shadow ROM B6AA 47 LD B,A ;B= character just input B6AB F5 PUSH AF ;Stack the flags B6AC FE0A CP #0A B6AE 2002 JR NZ,Q_INKEY$2 ;Jump unless this is a QL "enter" B6B0 060D LD B,#0D ;B= Spectrum "enter" character B6B2 F1 Q_INKEY$2 POP AF ;Restore the flags B6B3 78 LD A,B ;A= character to return B6B4 C9 RET ;Return B6B5 ED7B3D5C Q_INPUT_2 LD SP,(ERR_SP) ;Empty the machine stack down as ;far as ED_ERROR B6B9 E1 POP HL ;Drop the address ED_ERROR B6BA E1 POP HL ;HL= normal error return address ptr B6BB 223D5C LD (ERR_SP),HL ;Restore pointer to error return addr B6BE CDA1B6 Q_INPLOOP CALL Q_INKEY$ ;Read next character from network B6C1 3804 JR C,QINPSTORE ;Jump if character OK B6C3 28F9 JR Z,Q_INPLOOP ;Jump if still waiting B6C5 CF RST #08 ;Report "8, End of file" B6C6 07 DEFB #07 B6C7 FE0D QINPSTORE CP #0D B6C9 C8 RET Z ;Return (INPUT is now finished) if ;character is "enter" B6CA CD850F CALL ADD_CHAR_1 ;Store the char in the INPUT line B6CD FDCB377E BIT 7,(FLAG_X) B6D1 20EB JR NZ,QINPLOOP ;Loop back if doing INPUT LINE B6D3 FE22 CP #22 B6D5 CC850F CALL Z,ADD_CHAR_1 ;Double-up quote characters B6D8 18E4 JR Q_INPLOOP ;Go back for rest of INPUT B6DA CD2117 MODIFY_N CALL STR_DATA1 ;BC= STRMS data for stream supplied B6DD 78 LD A,B B6DE B1 OR C B6DF 280C JR Z,MOD_ERROR ;Error if stream not in use B6E1 2A4F5C LD HL,(CHANS) ;HL points to base of chan info area B6E4 09 ADD HL,BC ;HL points to 2nd byte of chan info B6E5 23 INC HL B6E6 23 INC HL B6E7 23 INC HL ;HL points to 5th byte of chan info B6E8 7E LD A,(HL) ;A= name of channel B6E9 FE4E CP "N" B6EB 2802 JR Z,MOD_QL ;Jump only if channel is "N" B6ED CF MOD_ERROR RST #08 ;Report "O, Invalid stream" B6EE 17 DEFB #17 B6EF 2B MOD_QL DEC HL B6F0 36B6 LD (HL),#B6 B6F2 2B DEC HL B6F3 368F LD (HL),#8F ;Store new input address B6F5 2B DEC HL B6F6 36B6 LD (HL),#B6 B6F8 2B DEC HL B6F9 3678 LD (HL),#78 ;Store new output address B6FB C9 RET ;[This last bit of code gets overwritten by the code in Part 5. JimG] B6FC 3E04 Z_DEMO LD A,#04 ;A= stream number to attach B6FE C34AB6 JP Z_OPEN - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 5 of 5, ZX Computing April 1987 This month, Toni Baker opens up a new channel to the 128's RAMdisc facility. [Note that the code in STREAMS.TAP "PART5"] [includes the code from "PART4". JimG] This is the final part in the Streams and Channels series, and this is an article for people who own either a Spectrum 128 or a Spectrum 128+2. This month's new channel introduces SERIAL FILES to the 128K machines. People who have Interface Ones and Microdrives will already be used to serial files on microdrive cartridge. In this case you open a microdrive file either as a READ file (if the file already exists on microdrive) or a WRITE file (in which case the file must be created on microdrive). You can print text to a WRITE file, and then CLOSE and OPEN it (so that it becomes a READ file) and you may then input your text as either strings or numbers into a BASIC variable. RAMdisc RAMdisc files work in exactly the same way, except that you don't need an Interface One. When you first OPEN a RAMdisc file for writing, a file with a given name is created on the Spectrum's so-called "silicon disc". Text or numbers may then be printed into this file. Once the channel is closed no more printing to the file is possible. The file may, however, be re-OPENed as a read file, in which case whatever is in the file may be input into a BASIC variable. As with microdrive files, RAMdisc files must be CLOSEd once all the data has been printed to, or input from, the file. If a WRITE file is not closed then some or all of the data may be lost, as it will not be cleared from a special buffer. If a READ file is not closed then the consequences are less serious, however, each RAMdisc channel requires more than 1/2K, which may only be reclaimed by closing the file. You should always close such a file once you have finished with it. Once a file has been opened, it will appear in the RAMdisc catalogue, which you can verify by typing CAT! in BASIC. It is impossible to LOAD a RAMdisc serial file using a LOAD command, however it Is possible to ERASE a RAMdisc serial file in the usual manner, by entering ERASE! "filename". You should never ERASE a RAMdisc serial file which is still in use (ie. which still has a stream attached to it). The machine code program does in fact protect itself from this eventually, so such an error would not be fatal - however, you will certainly get spurious results if you break this rule. Silicon disc The key to how the machine code program works is the manipulation of the memory organisation known as the silicon disc, or RAMdisc, which is normally used to save programs, data, or machine code for as long as the machine is switched on. RAMdisc files are much faster than microdrive files, but the whole of RAMdisc is erased when the machine is switched off. RAMdisc serial files will, of course, suffer from precisely these advantages and disadvantages. The RAMdisc memory itself is primarily organised by the CATALOGUE, which is an index to all files saved in RAMdisc. The CATALOGUE resides in RAM page seven. It is effectively a stack, which begins at address 7EBFF and grows downwards, with each entry taking twenty bytes. Figure Two shows the meanings of these twenty bytes, with IX pointing to the first of these bytes. At the end of the catalogue stack is a twenty byte "End of catalogue" marker, only three bytes of which are used. The system variable (SFNEXT) points to this marker, and is effectively the stack-pointer for this catalogue stack. So long as we keep the catalogue in its required format, we can manipulate the RAMdisc organisation itself from machine code. This is the aim of this issue's program. The RAMdisc files themselves begin in RAM page 1, and grow upwards through RAM pages 3, 4, 6 and 7 (care is taken to ensure that RAMdisc files do not collide with the catalogue stack). To avoid any problems with this strange page- numbering, all addresses in RAMdisc are "page-coded". This means that one register will hold a page-code, while another register-pair will hold an actual physical address. These page codes are 0, 1, 2, 3 and 4 sequentially for RAMdisc files, with page-code 5 being the conventional notation for normal (48K) RAM. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 1 R CHANNEL INFORMATION BLOCK --------------------------- IX+00 R_OUT Address of RAMdisc file output routine (=B92B) IX+02 R_IN Address of RAMdisc file input routine (=B7D4) IX+04 R_NAME Name of channel (="R") IX+05 R_IDEN New channel identifier (=1234h) IX+07 R_CLOSE Address of RAMdisc file close routine (=B960) IX+09 R_LEN Length of channel information block (=021B) IX+0B R_CHBYTE Pointer into buffer IX+0D R_CHREC Record number within file IX+0E R_CHNAME File name IX+18 R_CHFLAG Various flags, defined as follows: Bits 7 to 2: Not used Bit 1: Set if End-Of-File at end of record, reset otherwise Bit 0: Reset for a READ file, set for a WRITE file IX+19 R_RECLEN Length of record within buffer IX+1B R_BUFFER Buffer storing current record - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 2 CATALOGUE INDEX INFORMATION --------------------------- IX+00 SF_NAME File name IX+0A SF_START Page-coded address of start of file IX+0D SF_LEN Total number of bytes in file, including header info IX+10 SF_END Page-coded address of byte beyond end of file IX+13 SF_FLAG Reset unless catalogue information incomplete (ie. reset normally) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Channel "R" We shall call our new channel "R", which stands for RAMdisc serial file. This is not to be confused with the ROM's internal "R" channel, which is used for inserting bytes into normal (48K) dynamic RAM. Internal-channel-"R" is quite interesting in fact. It is permanently attached to stream minus-one, so to use it from machine code all you have to do is select stream minus-one as the current stream (by loading A with FF and calling address 1601h). It is impossible to use from BASIC. Prior to selecting stream minus-one the system variable K_CUR must be made to point somewhere into dynamic RAM. Printing to stream FF will then insert characters into dynamic memory at the point indicated by (K_CUR). Our new channel is also called "R", but its use is much more exciting - and of course it MAY be used from BASIC. We require a channel information block over 1/2K in size. Most of this is in fact a huge 0200h byte buffer. For reasons of speed, the buffer is used most of the time, with RAMdisc itself only being accessed once the end of the buffer is reached. It is of course important to realise that the location of a RAMdisc file is not constant - it may move either if another file is erased, or if more bytes are inserted into another RAMdisc serial file. This means that the file has to be relocated every time we wish to read or write into it. To save time we make use of a temporary buffer virtually all of the time. Figure One shows the actual organisation of the channel information block for our R channel. Note that R_CHREC, R_RECLEN, and bit one of R_CHFLAG are used only if the file is a READ file. The remaining variables are used for both types of file. To interface with BASIC, an example machine code program is appended to the end. Essentially, a RAMdisc serial file may be opened by loading the A register with the stream number of the stream to be opened, whilst the actual filename is stored in the system variable N_STR1 at address 5B67, and then calling the label R_OPEN. Four additional entry points are included, labelled OPEN_4, OPEN_5, CLOSE_4 and CLOSE_5. Calling OPEN_4 will open a RAMdisc serial file called "FILE1" and attach it to stream four. Similarly, calling OPEN_5 will open a RAMdisc serial file called "FILE2" and attach it to stream five. The routines CLOSE_4 and CLOSE_5 will of course close these new channels. This means that the new channel may easily be used in BASIC. Take a look at Figure Three. It contains a BASIC program which demonstrates the RAMdisc serial files at work, first as WRITE files, then as READ files. Try it - you may be surprised at how fast it all works. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 3 1000 REM WRITE FILE DEMO 1010 RANDOMIZE USR 47713: REM OPEN #4,"R_FILE1" 1020 RANDOMIZE USR 47720: REM OPEN #5,"R_FILE2" 1030 FOR i=1 TO 512 1040 INPUT "": PRINT i 1050 PRINT #4;2*i 1060 PRINT #5;i*i 1070 NEXT i 1080 RANDOMIZE USR 47736: REM CLOSE #4 1090 RANDOMIZE USR 47740: REM CLOSE #5 1100 REM READ FILE DEMO 1110 RANDOMIZE USR 47713: REM OPEN #4,"R_FILE1" 1120 RANDOMIZE USR 47720: REM OPEN #5,"R_FILE2" 1130 FOR i=1 TO 512 1140 INPUT #4;a 1150 INPUT #5;b 1160 PRINT a,b 1170 NEXT i 1180 RANDOMIZE USR 47736: REM CLOSE #4 1190 RANDOMIZE USR 47740: REM CLOSE #5 1200 STOP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Of course you won't always want your files to be called "FILE1" or "FILE2'; and you won't always want to use streams four or five. That is why the more general entry point R_OPEN is included, which, as has already been stated, requires that A contains the stream number, and (N_STR1) contains the filename (with trailing spaces if required). You will have to write your own machine code to patch any other combination into BASIC, utilising this routine. The potential for streams and channels is limitless. It is theoretically possible, for instance, to have a channel which utilises a RANDOM ACCESS FILE, or INDEXED FILE, in RAMdisc - though the program would have to be much more complicated. I have shown you enough of the potential use for streams and channels to whet your appetite a little, and there I shall leave you. If demand is high enough, I may return with more. Good programming everyone, and may the force be with you. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ERRATUM In OPEN_NEW at address B06D (Streams and Channels Part Two, January Issue, page 68) there is an instruction missing. The instruction is POP BC (hex code C1). It should be the eleventh instruction of the routine, occurring between OR C and JR Z,OPEN_NEW_2. Sorry about that. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The following are vectored routines in the Spectrum's ROM 0. Notice that there are two alternative such vector tables, one for the Spectrum 128 and one for the 128+2. This is because the two machines have different ROMs. If the ROM of the 128+2 is changed at some indefinite point in the future, then it will be necessary to rewrite this table further. (Spectrum 128 only) B6FC C3AC05 V_ERROR JP #05AC ;Generate an error report B6FF C3641C V_PAGE JP #1C64 ;Change current RAM page B702 C3971C V_NEWCAT JP #1C97 ;Create new entry in catalogue B705 C3F31C V_SPACE JP #1CF3 ;Ensure enough space in RAMdisc area B708 C3121D V_FIND JP #1D12 ;Find catalogue entry for filename B70B C3561D V_CATEND JP #1D56 ;Tidy up last catalogue entry (Spectrum 128+2 only) B6FC C3AC05 V_ERROR JP #05CB ;Generate an error report B6FF C3641C V_PAGE JP #1C83 ;Change current RAM page B702 C3971C V_NEWCAT JP #1CB6 ;Create new entry in catalogue B705 C3F31C V_SPACE JP #1D12 ;Ensure enough space in RAMdisc area B708 C3121D V_FIND JP #1D31 ;Find catalogue entry for filename B70B C3561D V_CATEND JP #1D75 ;Tidy up last catalogue entry The rest of the program will be the same, whichever version of the Spectrum you have. The following subroutine will decrement a page-coded address held in register triplet BHL. B70E 2B DEC_BHL DEC HL ;Decrement HL B70F 78 LD A,B ;A= page code B710 FE05 CP #05 B712 C8 RET Z ;Return if using standard RAM B713 CB74 BIT 6,H B715 C0 RET NZ ;Return unless HL has crossed ;a page boundary B716 CBF4 SET 6,H ;Correct address in HL B718 05 DEC B ;Decrement page code B719 C9 RET ;Return The following subroutine works a bit like a glorified LDDR instruction, which works in RAMdisc area as well as in standard memory. Its action is threefold: (1) Decrement BHL and BHL'; (2) Load one byte from address (BHL) to address (BHL'); (3) If BHL is not equal to CDE then go to step (1). B71A CD0EB7 RTRANSFER CALL DEC_BHL ;Decrement page-coded address in BHL B71D D9 EXX B71E CD0EB7 CALL DEC_BHL ;Decrement page-coded address in BHL' B721 D9 EXX B722 78 LD A,B ;A= paging code of FROM address B723 CDFFB6 CALL V_PAGE ;Page in the FROM memory page B726 7E LD A,(HL) ;A= byte to load B727 F5 PUSH AF ;Stack this byte B728 D9 EXX B729 78 LD A,B ;A= paging code of TO address B72A CDFFB6 CALL V_PAGE ;Page in the TO memory page B72D F1 POP AF ;A= byte to load B72E 77 LD (HL),A ;Load byte into memory as required B72F D9 EXX B730 78 RTRANSFE2 LD A,B ;A= paging code of FROM address B731 B9 CP C B732 20E6 JR NZ,RTRANSFER ;Loop back if not equal to the paging ;code of the limiting address B734 ED52 SBC HL,DE ;Set Zero flag if address = limit B736 19 ADD HL,DE ;(ADD HL,DE doesn't affect Zero flag) B737 20E1 JR NZ,RTRANSFER ;Loop back unless limit has been ;reached B739 C9 RET ;Return The next subroutine will calculate the page-coded address which is BC bytes further on from AHL. It assumes that BC is always less than 4000h. B73A 09 ADD_HL_BC ADD HL,BC ;Increment HL by BC bytes B73B FE05 CP #05 B73D C8 RET Z ;Return if using standard RAM B73E CB74 BIT 6,H B740 C0 RET NZ ;Return unless page boundary crossed B741 CBFC SET 7,H B743 CBF4 SET 6,H ;Correct address in HL B745 3C INC A ;Increment page code B746 C9 RET ;Return This subroutine will search for the file whose name is specified in the channel information area, in the RAMdisc catalogue, giving an error if the file does not exist. On exit IX will point to the catalogue entry. B747 DDE5 FIND_FILE PUSH IX B749 E1 POP HL ;HL points to channel information B74A 010E00 LD BC,#000E B74D 09 ADD HL,BC ;HL points to filename B74E 0E0A LD C,#0A ;BC= length of filename (ten) B750 11675B LD DE,N_STR1 ;DE points to system variable B753 EDB0 LDIR ;Copy filename into system variable B755 CD08B7 CALL V_FIND ;Find catalogue entry for this name B758 C0 RET NZ ;Return if file exists, with IX ;pointing to catalogue entry B759 CDFCB6 CALL V_ERROR ;Generate error message B75C 23 DEFB #23 ;"h File does not exist" The next subroutine is designed to match up the buffer for R-channel with the corresponding region of RAMdisc memory. The subroutine will leave BHL pointing to the first byte beyond the RAMdisc segment, CDE pointing to the start of the RAMdisc segment, and BHL' pointing to the first byte beyond the corresponding region in the R-channel buffer. It will also signal whether or not this is an end-of-file block. It requires that IX initially points to the channel information block. B75D DDE5 R_MATCH PUSH IX ;Stack channel info address B75F DD7E0D LD A,(R_CHREC) B762 F5 PUSH AF ;Stack record number to match B763 CD47B7 CALL FIND_FILE ;IX points to catalogue entry B766 C1 POP BC ;B= record number B767 CB20 SLA B ;B= record number x 2 B769 0E01 LD C,#01 ;BC= 200h * record number + 1 B76B 37 SCF B76C 08 EX AF,AF' ;Signal "End of file block" B76D DD6E0D LD L,(SF_LEN) B770 DD660E LD H,(SF_LEN+1) B773 DD7E0F LD A,(SF_LEN+2) ;AHL= length of file (17 bit) B776 A7 AND A B777 ED42 SBC HL,BC B779 DE00 SBC A,#00 ;AHL= length of remainder of file B77B A7 AND A B77C 2008 JR NZ,RM_NO_EOF ;Jump if high part of AHL is non-zero B77E 110102 LD DE,#0201 B781 ED52 SBC HL,DE B783 19 ADD HL,DE B784 3804 JR C,RM_EOF ;Jump if AHL less than 0201h B786 210002 RM_NO_EOF LD HL,#0200 ;HL= length of record (0200h max) B789 08 EX AF,AF' ;Signal "Not end of file block" B78A EB RM_EOF EX DE,HL ;DE= length of record B78B DD6E0A LD L,(SF_START) B78E DD660B LD H,(SF_START+1) B791 DD7E0C LD A,(SF_START+2);AHL= coded addr of start of file B794 CD3AB7 CALL ADD_HL_BC ;AHL= coded addr of RAMdisc segment B797 C5 PUSH BC ;Stack 200h * record number + 1 B798 D5 PUSH DE ;Stack length of record B799 E5 PUSH HL B79A F5 PUSH AF ;Stack page-coded address of segment B79B 42 LD B,D B79C 4B LD C,E ;BC= length of record B79D CD3AB7 CALL ADD_HL_BC ;AHL= page-coded address of byte ;following RAMdisc segment B7A0 47 LD B,A ;BHL= this address B7A1 F1 POP AF B7A2 4F LD C,A B7A3 D1 POP DE ;CDE= page-coded address of segment B7A4 D9 EXX ;Use alternative registers B7A5 D1 POP DE ;DE'= length of record B7A6 C1 POP BC ;BC'= 200h * record number + 1 B7A7 DDE1 POP IX ;IX points to channel info area B7A9 DD7319 LD (R_RECLEN),E B7AC DD721A LD (R_RECLEN+1),D;Store length of record B7AF DDE5 PUSH IX B7B1 E1 POP HL ;HL' points to channel information B7B2 011B00 LD BC,#001B B7B5 09 ADD HL,BC ;HL' points to the R-channel buffer B7B6 19 ADD HL,DE ;HL' points to byte following ;current record B7B7 0605 LD B,#05 ;B'= 05, signalling "Standard RAM" B7B9 D9 EXX ;Use normal registers B7BA DDCB188E RES 1,(R_CHFLAG) ;Signal "Not end of file block" B7BE 08 EX AF,AF' B7BF D0 RET NC ;Return unless end of file block B7C0 DDCB18CE SET 1,(R_CHFLAG) ;Signal "End of file block" B7C4 C9 RET ;Return This subroutine actually assigns the R-channel buffer in preparation for use with a READ channel. Note that it calls the RTRANSFER subroutine from label RTRANSFE2 in order to deal with the zero case, when the buffer is to be considered empty. B7C5 CD5DB7 R_ASSIGN CALL R_MATCH ;Match buffer with RAMdisc segment B7C8 CD30B7 CALL RTRANSFE2 ;Copy bytes into buffer B7CB DD360B00 RBUFFEXIT LD (R_CHBYTE),#00 B7CF DD360C00 LD (R_CHBYTE+1),#00 ;Reset pointer into buffer B7D3 C9 RET ;Return There now follows the INPUT routine for channel R. It isolates INPUT from INKEY$ and deals with each accordingly. B7D4 2A3D5C R_INPUT LD HL,(ERR_SP) ;HL points to error return address B7D7 5E LD E,(HL) B7D8 23 INC HL B7D9 56 LD D,(HL) ;DE= error return address B7DA 217F10 LD HL,ED_ERROR B7DD A7 AND A B7DE ED52 SBC HL,DE B7E0 2021 JR NZ,R_INKEY ;Jump if dealing with INKEY$ B7E2 ED7B3D5C LD SP,(ERR_SP) ;Clear machine stack as far as ;return from EDITOR routine B7E6 E1 POP HL B7E7 E1 POP HL B7E8 223D5C LD (ERR_SP),HL ;Restore normal error return address B7EB CD03B8 R_INPLOOP CALL R_INKEY ;Input a single character into A B7EE FE0D CP #0D B7F0 C8 RET Z ;Return if character is "enter" B7F1 FDCB377E BIT 7,(FLAG_X) B7F5 2007 JR NZ,R_INPUT_2 ;Jump if doing INPUT LINE B7F7 FE22 CP #22 B7F9 2003 JR NZ,R_INPUT_2 ;Jump unless character is "quotes" B7FB CD850F CALL ADD_CHAR_1 ;Register quotes twice B7FE CD850F R_INPUT_2 CALL ADD_CHAR_1 ;Insert character into INPUT area B801 18E8 JR R_INPLOOP ;Loop back to input rest of string The following routine inputs a single character from an R-channel and returns it in the A register. B803 CD005B R_INKEY CALL SWAP ;Page in ROM 0 B806 2A5A5B LD HL,(RETADDR) B809 E5 PUSH HL ;Stack return address in ROM 0 B80A D9 EXX B80B C5 PUSH BC B80C D5 PUSH DE B80D E5 PUSH HL ;Stack alternative register set B80E DD2A515C LD IX,(CURCHL) ;IX points to channel information B812 DDCB1846 BIT 0,(R_CHFLAG) B816 2804 JR Z,R_INKEY_2 ;Jump if this is a READ file B818 CDFCB6 R_ERROR CALL V_ERROR ;Generate report code B81B 1D DEFB #1D ;"b Wrong file type" B81C DD5E0B R_INKEY_2 LD E,(R_CHBYTE) B81F DD560C LD D,(R_CHBYTE+1);DE= position of next byte to read B822 DDCB184E BIT 1,(R_CHFLAG) B826 280F JR Z,RINKYREAD ;Jump unless this is an EOF block B828 DD6E19 LD L,(R_RECLEN) B82B DD661A LD H,(R_RECLEN+1);HL= length of current record B82E A7 AND A B82F ED52 SBC HL,DE B831 2004 JR NZ,RINKYREAD ;Jump unless we have reached the ;end of the (EOF) record B833 CDFCB6 CALL V_ERROR ;Generate error report B836 07 DEFB #07 ;"8 End of file" B837 DDE5 RINKYREAD PUSH IX B839 E1 POP HL ;HL points to channel information B83A 011B00 LD BC,#001B B83D 09 ADD HL,BC ;HL points to buffer B83E 19 ADD HL,DE ;HL points to next byte to read B83F 7E LD A,(HL) ;A= byte which INKEY$ must return B840 F5 PUSH AF B841 13 INC DE ;Increment pointer B842 DD730B LD (R_CHBYTE),E B845 DD720C LD (R_CHBYTE+1),D;Store incremented pointer B848 15 DEC D B849 15 DEC D B84A 2006 JR NZ,RINKYEXIT ;Jump unless buffer to be renewed B84C DD340D INC (R_CHREC) ;Increment record number B84F CDC5B7 CALL R_ASSIGN ;Assign and reset buffer B852 F1 RINKYEXIT POP AF ;A= byte just read from buffer B853 37 SCF ;Set Carry, so that INKEY$ # works ;properly B854 E1 RINOUEXIT POP HL B855 D1 POP DE B856 C1 POP BC B857 D9 EXX ;Restore alternative registers B858 E1 R_EXIT POP HL ;HL= return address into ROM 0 B859 225A5B LD (RETADDR),HL ;Store in system variable B85C C3005B JP SWAP ;Page in ROM 1 and return The next subroutine is designed to insert additional bytes into an already existing file stored in RAMdisc. Any files which need to be moved in order to make room for these extra bytes will be so moved, and re-indexed to accomodate. The subroutine should be entered with AHL containing the page-coded address at which to insert the bytes, and BC containing the number of bytes to insert. B85F C5 RMAKEROOM PUSH BC ;Stack number of bytes to insert B860 E5 PUSH HL B861 F5 PUSH AF ;Stack page-coded address at ;which to insert B862 AF XOR A ;A= 00, Carry flag reset B863 67 LD H,A B864 6F LD L,A ;AHL= zero B865 ED42 SBC HL,BC B867 9F SBC A,A ;AHL= minus no. of bytes to insert B868 CD05B7 CALL V_SPACE ;Ensure enough room for extra bytes B86B F1 POP AF B86C E1 POP HL ;AHL= address at which to insert B86D C1 POP BC ;BC= number of bytes to insert B86E C5 PUSH BC B86F E5 PUSH HL B870 F5 PUSH AF B871 3E04 LD A,#04 B873 CDFFB6 CALL V_PAGE ;Select page containing catalogue B876 DD2A835B LD IX,(SF_NEXT) ;IX points to "End of cat" index B87A DD6E0A LD L,(SF_START) B87D DD660B LD H,(SF_START+1) B880 DD7E0C LD A,(SF_START+2);AHL= page-coded address of first ;spare byte in RAMdisc area B883 F5 PUSH AF B884 E5 PUSH HL ;Stack this address B885 CD3AB7 CALL ADD_HL_BC ;AHL= page-coded address of first ;RAMdisc byte which will remain spare ;after more bytes are inserted B888 47 LD B,A ;BHL= this address B889 D9 EXX ;BHL'= this address B88A E1 POP HL B88B C1 POP BC ;BHL points to 1st spare byte (old) B88C F1 POP AF B88D D1 POP DE ;ADE= address at which to insert B88E 4F LD C,A ;CDE= address at which to insert B88F D5 PUSH DE B890 F5 PUSH AF ;Stack this address B891 CD30B7 CALL RTRANSFE2 ;Move bytes which need to be moved B894 3E04 LD A,#04 B896 CDFFB6 CALL V_PAGE ;Select page containing catalogue B899 C1 POP BC B89A D1 POP DE ;BDE= position of insertion B89B DD6E0A R_MR_LOOP LD L,(SF_START) ;AHL= previous page-coded address of B89E DD660B LD H,(SF_START+1);a RAMdisc file (or next spare byte) B8A1 DD7E0C LD A,(SF_START+2);which may have been moved B8A4 B8 CP B B8A5 382E JR C,R_MRFOUND ;Jump if file address precedes ;point of insertion B8A7 ED52 SBC HL,DE B8A9 19 ADD HL,DE B8AA 3829 JR C,R_MRFOUND ;Jump if file address precedes ;point of insertion B8AC EB EX DE,HL B8AD E3 EX (SP),HL B8AE EB EX DE,HL ;DE= number of bytes inserted B8AF 19 ADD HL,DE B8B0 3005 JR NC,R_MR_ADDR B8B2 CBFC SET 7,H B8B4 CBF4 SET 6,H B8B6 3C INC A ;AHL= new address of file B8B7 EB R_MR_ADDR EX DE,HL B8B8 E3 EX (SP),HL B8B9 EB EX DE,HL ;BDE= position of insertion B8BA DD750A LD (SF_START),L B8BD DD740B LD (SF_START+1),H B8C0 DD770C LD (SF_START+2),A;Store new start address of file B8C3 C5 PUSH BC B8C4 011400 LD BC,#0014 B8C7 DD09 ADD IX,BC ;IX points to index for next file B8C9 C1 POP BC ;BDE= position of insertion B8CA DD7510 LD (SF_END),L B8CD DD7411 LD (SF_END+1),H B8D0 DD7712 LD (SF_END+2),A ;Store new address for next file B8D3 18C6 JR R_MR_LOOP ;Loop back to deal with this file B8D5 DD6E0D R_MRFOUND LD L,(SF_LEN) B8D8 DD660E LD H,(SF_LEN+1) B8DB DD7E0F LD A,(SF_LEN+2) ;AHL= previous length of file B8DE EB EX DE,HL B8DF E3 EX (SP),HL B8E0 EB EX DE,HL ;DE= number of bytes inserted B8E1 19 ADD HL,DE B8E2 CE00 ADC A,#00 ;AHL= new length of file B8E4 DD750D LD (SF_LEN),L B8E7 DD740E LD (SF_LEN+1),H B8EA DD770F LD (SF_LEN+2),A ;Store new length of file B8ED 78 LD A,B ;A= page-code of point of insertion B8EE 42 LD B,D B8EF 4B LD C,E ;BC= number of bytes inserted B8F0 E1 POP HL ;AHL= page-coded address of point ;at which bytes were inserted B8F1 C9 RET ;Return The following subroutine will transfer the contents of the R-channel buffer into the corresponding RAMdisc file. B8F2 DD4E0B R_STORE LD C,(R_CHBYTE) B8F5 DD460C LD B,(R_CHBYTE+1);BC= number of bytes in buffer B8F8 DDE5 PUSH IX ;Stack address of R-channel info B8FA C5 PUSH BC ;Stack number of bytes in buffer B8FB CD47B7 CALL FIND_FILE ;IX points to file entry in cat. B8FE C1 POP BC ;BC= number of bytes in buffer B8FF DD6E10 LD L,(SF_END) B902 DD6611 LD H,(SF_END+1) B905 DD7E12 LD A,(SF_END+2) ;AHL= page-coded address of first ;byte beyond end of file B908 CD5FB8 CALL RMAKEROOM ;Insert enough room for contents ;of buffer B90B DDE1 POP IX ;IX points to channel information B90D CD3AB7 CALL ADD_HL_BC ;AHL points one byte beyond the ;last of the new bytes B910 C5 PUSH BC ;Stack number of bytes in buffer B911 47 LD B,A ;BHL= address of last new byte + 1 B912 D9 EXX ;BHL'= address of last new byte + 1 B913 DDE5 PUSH IX B915 E1 POP HL ;HL= address of channel information B916 011B00 LD BC,#001B B919 09 ADD HL,BC ;HL points to start of buffer B91A C1 POP BC ;BC= number of bytes in buffer B91B E5 PUSH HL ;Stack address of start of buffer B91C 09 ADD HL,BC ;HL points to byte beyond buffer B91D D1 POP DE ;DE points to start of buffer B91E 010505 LD BC,#0505 ;BHL and CDE now page-coded addresses B921 CD30B7 CALL RTRANSFE2 ;Copy buffer into RAMdisc area B924 78 LD A,B ;A= 05 B925 CDFFB6 CALL V_PAGE ;Page in normal RAM B928 C3CBB7 JP RBUFFEXIT ;Reset pointer into buffer and return Now comes the output routine, whose job it is to print the character stored in the A register to an R channel (ie. to store it firstly in the buffer, and ultimately in a RAMdisc file). B92B CD005B R_PRINT CALL SWAP ;Page in ROM 0 B92E 2A5A5B LD HL,(RETADDR) B931 E5 PUSH HL ;Stack return address into ROM 0 B932 D9 EXX B933 C5 PUSH BC B934 D5 PUSH DE B935 E5 PUSH HL ;Stack alternative register set B936 DD2A515C LD IX,(CURCHL) ;IX points to channel information B93A DDCB1846 BIT 0,(R_CHFLAG) B93E CA18B8 JP Z,R_ERROR ;Error if this is a READ file B941 DD5E0B LD E,(R_CHBYTE) B944 DD560C LD D,(R_CHBYTE+1);DE= number of bytes in buffer B947 DDE5 PUSH IX B949 E1 POP HL ;HL points to channel information B94A 011B00 LD BC,#001B B94D 09 ADD HL,BC ;HL points to start of buffer B94E 19 ADD HL,DE ;HL points to next spare byte B94F 77 LD (HL),A ;Store byte in buffer B950 13 INC DE ;DE= new number of bytes in buffer B951 DD730B LD (R_CHBYTE),E B954 DD720C LD (R_CHBYTE+1),D;Store new number of chars in buffer B957 15 DEC D B958 15 DEC D B959 CCF2B8 CALL Z,R_STORE ;If buffer is now full, then empty ;contents into RAMdisc file B95C A7 AND A ;Reset Carry B95D C354B8 JP RINOUEXIIT ;Jump to exit routine Next we have the routine to CLOSE an R channel. All that is necessary is that the buffer contents be ignored (READ file) or stored in RAMdisc (WRITE file). B960 CD005B R_CLOSE CALL SWAP ;Page in ROM 0 B963 2A5A5B LD HL,(RETADDR) B966 E5 PUSH HL ;Stack return address into ROM 0 B967 DDE5 PUSH IX ;Stack pointer to channel info B969 2A3D5C LD HL,(ERR_SP) ;HL points to error return address B96C E5 PUSH HL ;Stack this pointer B96D 21FEFF LD HL,#FFFE B970 39 ADD HL,SP ;HL= SP minus two B971 223D5C LD (ERR_SP),HL ;Set new error return address B974 DDCB1846 BIT 0,(R_CHFLAG) B978 C4F2B8 CALL NZ,R_STORE ;If this is a WRITE file, then empty ;buffer contents into RAMdisc B97B E1 POP HL ;NOTE: This is also the return point ;from any errors that may have ;occurred during R_STORE B97C 223D5C LD (ERR_SP),HL ;Restore error pointer to normal B97F DDE1 POP IX ;Restore channel info pointer B981 215827 R_OC_EXIT LD HL,#2758 B984 D9 EXX ;HL'= 2758 to prevent crash B985 C358B8 JP R_EXIT ;Jump to exit routine At last we have the routine to OPEN an R channel. On entry the A register must contain the stream number to which the channel is to be attached, and the 10- byte system variable N_STR1 must contain the filename of the READ or WRITE file to be opened. If the filename is less than 10 characters long then it should be followed by trailing spaces. B988 CD005B R_OPEN CALL SWAP ;Page in ROM 0 B98B 2A5A5B LD HL,(RETTADDR) B98E E5 PUSH HL ;Stack return address into ROM 0 B98F F5 PUSH AF ;Stack stream number B990 3E52 LD A,"R" ;A= name of this channel ("R") B992 CD94B5 CALL SEARCHALL ;Search for an existing R channel B995 381F JR C,R_OP_OK ;Jump if none found B997 DDE5 R_OP_LOOP PUSH IX B999 E1 POP HL ;HL points to channel information ;for already existing R channel B99A 010E00 LD BC,#000E B99D 09 ADD HL,BC ;HL points to filename for ;already existing R channel B99E 11675B LD DE,N_STR1 ;DE points to intended filename ;for this channel B9A1 060A LD B,#0A ;B= length of filename B9A3 1A R_OP_NAME LD A,(DE) B9A4 13 INC DE B9A5 BE CP (HL) B9A6 23 INC HL B9A7 2006 JR NZ,#R_OPRETRY ;Jump if filenames are different B9A9 10F8 DJNZ R_OP_NAME ;Test all 10 characters of filename. ;If filenames are identical then B9AB CDFCB6 R_OPERROR CALL V_ERROR ;generate error report B9AE 20 DEFB #20 ;"e File already exists" B9AF 1652 R_OPRETRY LD D,"R" ;D= name of this channel ("R") B9B1 CDAAB5 CALL SEARCHNXT ;Search for next existing R channel B9B4 30E1 JR NC,R_OP_LOOP ;Loop back if one found B9B6 CD08B7 R_OP_OK CALL V_FIND ;Search for RAMdisc file with ;given name B9B9 F5 PUSH AF ;Stack the Zero flag B9BA 2811 JR Z,R_OP_CONT ;Jump if no file found (ie. if this ;is to be a WRITE file) B9BC DD6E0A LD L,(SF_START) B9BF DD660B LD H,(SF_START+1) ;[The next line was incorrectly printed as DD7E0B in the magazine. JimG] B9C2 DD7E0C LD A,(SF_START+2);AHL= page-coded address of file ;with given name B9C5 CDFFB6 CALL V_PAGE ;Select page containing first byte ;of this file B9C8 7E LD A,(HL) ;A= type-of-file code B9C9 FE04 CP #04 B9CB 20DE JR NZ,R_OPERROR ;Give error unless a READ file B9CD 3E05 R_OP_CONT LD A,#05 B9CF CDFFB6 CALL V_PAGE ;Page in normal RAM B9D2 F1 POP AF ;Retrieve Zero flag B9D3 08 EX AF,AF' ;Store in A' B9D4 F1 POP AF ;A= stream number B9D5 08 EX AF,AF' ;A'= stream number B9D6 F5 PUSH AF ;Stack the Zero flag, which determines ;whether this is a READ or WRITE file B9D7 3E52 LD A,"R" ;A= name of this channel ("R") B9D9 011B02 LD BC,#021B ;BC= length of channel info block B9DC 11D4B7 LD DE,R_INPUT ;DE= address of input routine B9DF 212BB9 LD HL,R_PRINT ;HL= address of output routine B9E2 DD2160B9 LD IX,R_CLOSE ;IX= address of close routine B9E6 EF RST #28 B9E7 6DB0 DEFW #B06D,OPEN_NEW;Create channel information block B9E9 DDE5 PUSH IX B9EB E1 POP HL ;HL points to channel information B9EC 010B00 LD BC,#000B B9EF 09 ADD HL,BC ;HL points to variable R_CHBYTE B9F0 70 LD (HL),B B9F1 23 INC HL B9F2 70 LD (HL),B ;Reset R_CHBYTE B9F3 23 INC HL B9F4 70 LD (HL),B ;Reset R_CHREC B9F5 23 INC HL B9F6 EB EX DE,HL ;DE points to R_CHNAME B9F7 21675B LD HL,N_STR1 ;HL points to filename given B9FA 0E0A LD C,#0A ;DE= length of filename (ten) B9FC EDB0 LDIR ;Copy filename into channel info B9FE F1 POP AF ;Retrieve Zero flag B9FF 2809 JR Z,R_OPWRITE ;Jump if this is to be a WRITE file BA01 DDCB1886 RES 0,(R_CHFLAG) ;Signal "This is a READ file" BA05 CDC5B7 CALL R_ASSIGN ;Assign buffer from RAMdisc file BA08 1840 JR R_OP_EXIT ;Jump to exit routine BA0A DDCB18C6 R_OPWRITE SET 0,(R_CHFLAG) ;Signal "This is a WRITE file" BA0E CD02B7 CALL V_NEWCAT ;Create new catalogue entry BA11 21FFFF LD HL,#FFFF BA14 7C LD A,H ;AHL= minus one BA15 CD05B7 CALL V_SPACE ;Ensure enough room for one byte BA18 3E04 LD A,#04 BA1A CDFFB6 CALL V_PAGE ;Select page containing catalogue BA1D DD6E0A LD L,(SF_START) BA20 DD660B LD H,(SF_START+1) BA23 DD7E0C LD A,(SF_START+2);AHL= page-coded address of first ;spare byte in RAMdisc BA26 F5 PUSH AF ;Stack page-code BA27 CDFFB6 CALL V_PAGE ;Select page containing first ;spare byte in RAMdisc BA2A F1 POP AF ;AHL points to first spare byte BA2B 3604 LD (HL),#04 ;Store 04 as type-of-file code BA2D 010100 LD BC,#0001 BA30 CD3AB7 CALL ADD_HL_BC ;AHL= page-coded address of new ;first spare byte in RAMdisc BA33 5F LD E,A ;EHL= this address BA34 3E04 LD A,#04 BA36 CDFFB6 CALL V_PAGE ;Select page containing catalogue BA39 DD7510 LD (SF_END),L BA3C DD7411 LD (SF_END+1),H BA3F DD7312 LD (SF_END+2),E ;Store address of end of file BA42 CD0BB7 CALL V_CATEND ;Tidy up catalogue entry BA45 3E05 LD A,#05 BA47 CDFFB6 CALL V_PAGE ;Select normal RAM BA4A C381B9 R_OP_EXIT JP R_OC_EXIT ;Jump to exit routine And finally, we have the routines which integrate the R channel with BASIC. These are only example routines, and you may of course rewrite them to your own specifications. OPEN_4 will open stream four to a serial file called FILE1; OPEN_5 will open stream five to a serial file called FILE2; CLOSE_4 will close stream four; and CLOSE_5 will close stream five. BA4D 46494C4531 DEFM FILE1 2020202020 DEFM five spaces ;Name of first file 46494C4532 DEFM FILE2 2020202020 DEFM five spaces ;Name of second file BA61 3E04 OPEN_4 LD A,#04 ;A= stream number BA63 214DBA LD HL,FILE_1 ;HL points to filename BA66 1805 JR OPEN_4_5 ;Jump to open stream BA68 3E05 OPEN_5 LD A,#05 ;A= stream number BA6A 2157BA LD HL,FILE_2 ;HL points to filename BA6D 11675B OPEN_4_5 LD DE,N_STR1 ;DE points to system variable BA70 010A00 LD BC,#000A ;BC= length of filename (ten) BA73 EDB0 LDIR ;Copy filename into system variable BA75 C388B9 JP R_OPEN BA78 3E04 CLOSE_4 LD A,#04 ;A= stream number BA7A 1802 JR CLOSE_4_5 BA7C 3E05 CLOSE_5 LD A,#05 ;A= stream number BA7E C300B0 CLOSE_4_5 JP CLOSE_NEW ;Jump to close channel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -