Channels and streams.

The place for codemasters or beginners to talk about programming any language for the Spectrum.
Post Reply
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Channels and streams.

Post by Joefish »

Anyone got any experience of programming their own channels and streams for PRINT #, INPUT #, INKEY$ #, etc.?

I know there's some code kicking around from YS #7 on how to divert these commands to your own functions, and I've donee it successfully hijacking stream #3 (LPRINT) to my own routine. Then when you do PRINT #3 or LPRINT the ROM calls you back one character at a time in the A register and ends with a RETURN code. I gather it saves the registers it needs in the alternate set so you don't need to PUSH anything unless you use the alternate registers.

But I have no idea how to go about providing a response to an INPUT # or INKEY$ # routine. I'd like to be able to do it.

Jason R
Nomad
Manic Miner
Posts: 600
Joined: Thu Dec 28, 2017 12:38 pm

Re: Channels and streams.

Post by Nomad »

A lot of the microdrive code relies on you setting up its own stream for the data. There is a good book that gives examples on how its done (for read write). I figure you could expand on that. I think the only problem is if it uses code from the shadow rom - but you could just replicate it if you needed to..

https://spectrumcomputing.co.uk/index.p ... id=2000072

Cooke's book is very nice, its full of examples on how to actually program the Interface I.

Image

I usually pan the books for spectrum but this one was really well written and clear.

I think if you have this with the Ian Logan Microdrive rom book you will be cooking on gas.
https://spectrumcomputing.co.uk/index.p ... id=2000365

Logan's book on the Microdrive Rom is just as good as his main rom books.

My guess (because I never seen a copy) is that 25 multi-user programs is going to be packed full of examples of stream management.

https://spectrumcomputing.co.uk/index.p ... id=2000006

I once found a pretty wacky article for using the microdrive buffers to get better performance. It's worth a look as it shows the code and the explanation why he did what he did..

viewtopic.php?f=6&t=445&p=5886&hilit=microdrive#p5886
User avatar
PeterJ
Site Admin
Posts: 6854
Joined: Thu Nov 09, 2017 7:19 pm
Location: Surrey, UK

Re: Channels and streams.

Post by PeterJ »

hikoki
Manic Miner
Posts: 576
Joined: Thu Nov 16, 2017 10:54 am

Re: Channels and streams.

Post by hikoki »

This book may be interesting for Joefish : https://spectrumcomputing.co.uk/index.p ... id=2000359
It was scanned recently here : http://trastero.speccy.org/cosas/Libros/interfases.htm
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Re: Channels and streams.

Post by Joefish »

OK, mostly from the Toni Baker stuff Peter sent me:
https://spectrumcomputing.co.uk/index.p ... 6&id=24050

You can use this code as a simple way to add functions to BASIC. Once set up, use LPRINT to send it your new commands as strings, then use INPUT #3 or INKEY$#3 to read back responses from it.

I need these definitions:

Code: Select all

;System Variables:
	CHANS		equ 23631   ;Channel / Stream information
	ERR_SP		equ 23613   ;Pointer to current error handler
	
;ROM Addresses:
	ED_ERROR            equ 4223    ;The INPUT editor error handler
	ADD_CHAR            equ 3969    ;Routine to add char to INPUT queue 
	
;My variables:
	error_code		defb 'X'
Then this code installs my new stream handlers for channel 3. This replaces the defaults for the Sinclair Printer, so now I can use LPRINT to send command strings to my custom routines. This is what CharAde is based on:

Code: Select all

		;Hijack the LPRINT Channel, #3:
		ld hl,(CHANS)
		ld bc,5*3
		add hl,bc
		
		ld bc,my_print
		ld de,my_input
		ld (hl),c
		inc hl
		ld (hl),b
		inc hl
		ld (hl),e
		inc hl
		ld (hl),d
		
		;That's it for now:
		ret
The replacement PRINT routine gets called whenever you do LPRINT or PRINT #3. It gets called once for each character in the PRINT string with that character code in A. And lastly is called with the RETURN character 13.

Code: Select all

my_print
	;Respond to the characters fed in one at a time in A.
	;Maybe queue them up until we have a whole command, then act on it.
	;...
	
	ret
The replacment INPUT routine can return a character (here an error code byte I've previously stored at address error_code) either to an INPUT or an INKEY$ command, e.g. INPUT #3;A$ or LET A$=INKEY$#3 I didn't even know you could do INKEY$#3 until I tried this!
First the code checks which error handler the ROM has set up, to tell between an INPUT and an INKEY$ command.
To respond to an INKEY$, simply put the character code in A and set the carry flag with SCF, then RET. If there is no character to returm, then just RET without setting the carry flag.
To respond to an INPUT, first the stack needs to be cut back so that we can just complete the INPUT with a RET and not have it fall into an error handler. Then the ROM routine that extends the input buffer and adds a characater to it needs to be called, once for each character. Finally just RET. There's no need to add quotes or a RETURN at the end. Just note that if you want to return a quotes character, to add it twice, as if you were typing it as part of a string.

Code: Select all

my_input
        ld hl,(ERR_SP)      ;What error handler is in place?
        ld e,(hl)
        inc hl
        ld d,(hl)
        ld hl,ED_ERROR      ;Is it the INPUT error handler?
        and a               ;(Clear Carry Flag)
        sbc hl,de           ;Compare handler addresses
        jr nz,do_inkey      ;If no match, assume command was INKEY$#3, not INPUT #3
        
do_input
        ld sp,(ERR_SP)
        pop hl
        pop hl
        ld (ERR_SP),hl          ;Cleared stack down as far as return from INPUT edit routine
        
        ld a,(error_code)       ;Get the character code to respond with
        or a                    ;Test if 0
        ret z                   ;And exit
        
        call ADD_CHAR           ;If not 0, put in INPUT queue,
        ret                     ;then exit
        
do_inkey
        ;On an INKEY$#3 command, return the latest error code.
        ;To return a character, put it in A then do SCF to signal
        ;something is returned.
        ;In our case, only set the carry flag if the code > 0, like this:
        
        ld a,(error_code)       ;Get the character code to respond with
        cp 1                    ;Sets the carry flag if A is 0
        ccf                     ;Inverts the carry flag
	ret                     ;Back to ROM handler
In these callback handlers it's safe to use the ABCDEHL registers, but the system expects the alternate registers and IX and IY to remain unchanged. If you must use those registers, PUSH them then POP them again before returning (though watch out for the stack adjustments in the INPUT handler).
User avatar
djnzx48
Manic Miner
Posts: 729
Joined: Wed Dec 06, 2017 2:13 am
Location: New Zealand

Re: Channels and streams.

Post by djnzx48 »

Whoa, streams work with INKEY$ as well? I never knew you could do that, it could make it a lot easier to get realtime hardware input from BASIC. I wonder if it's possible to do something like map an input stream to a joystick and then use INKEY$ to get the result instead of having to mess about with IN.
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Re: Channels and streams.

Post by Joefish »

Well, at some point I'll overhaul CharAde with this technique.
I was thinking you could return a string of three characters; the first being 'U', 'D' or SPACE. The second is 'L', 'R' or SPACE, and the third is 'F' or SPACE. Or, you could use INKEY$ to read back these characters, or maybe each direction, one at a time.

Isn't there a syntax to print a prompt in front of the INPUT cursor? Not sure how that works or how it would translate into streams, but it could be used to issue a command then read the result.

Though if you only want a joystick helper, you could code a dedicated INPUT stream just to do the string method. Note that adding a new stream needs a bit more than this code. This example just diverts an existing stream.
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Re: Channels and streams.

Post by Joefish »

Actually, you could just do it with the INPUT side of things.

For those that don't know, PRINT #3;A$ and LPRINT A$ do exactly the same thing. Output stream #3 is pre-set to feed the printer. The equivalent A$=INPUT #3 stream is also pre-configured by the ROM, but it doesn't do anything.

My thoughts for a custom keyboard/joystick function are if you call INKEY$#3 it swaps between keyboard (QAOPM/SPACE) and (Kempston) joystick and returns a character 'K' or 'J' to tell you which; just do INKEY$#3 again if you don't like the answer. Maybe merge Sinclair#1 and QAOPM/SPACE in the keyboard read (since they don't overlap), so you could use two joysticks at a pinch.

Then when you call INPUT #3 you're guaranteed a three-character string in response (don't worry - it doesn't freeze up until you type something or force you to press ENTER or anything). The first character is U or D or a SPACE for the vertical control, then L or R or SPACE for horizontal control, then F or SPACE for FIRE control. You could do a five-character string with UDLRF or SPACEs, or 1s and 0s in the five positions.

I suppose you could do something like INPUT #3;V,H,F and return three numeric values, 1, -1 or 0 for the vertical, the same for horizontal, and then 1 or 0 for fire. Though I haven't experimented with how to hand back multiple responses like that. I think the stack meddling that goes on in skipping the keyboard input editing functions may screw things up. Or, if the input function just gets called three times entirely separately, it would have no real way of knowing which of the three numbers to return. So I think the string method is preferable.

My elder brother used to take the P?s? out of me for writing a really crude PAC-MAN type thing that used INPUT since I didn't understand INKEY$, and you had to press ENTER after each movement! Now keyboard and twin-joystick support, including diagonals, all from an INPUT would show him. Except he died 20 years ago. Clogs popped. Full-time daisy pusher-upperer. Still, the moral victory would be mine. And I've got his LEGO.
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Re: Channels and streams.

Post by Joefish »

OK, I put the Game&Watch thing on hold for the moment and worked on this joystick reader.
It works now - I should probably make a .TAP of it. You can read either QAOPM/SPACE and Sinclair 2 combined, or Kempston IN 31. INPUT #3 returns a 3-character string breaking down the directions, and making it much easier to work with the diagonals from BASIC.
User avatar
Joefish
Rick Dangerous
Posts: 2041
Joined: Tue Nov 14, 2017 10:26 am

Re: Channels and streams.

Post by Joefish »

Interesting issue I just came across - BASIN doesn't accept INKEY$#3 as valid syntax!

Might change the INIT function so you can call it at address 65000 or 65001, and the difference defines Keyboard or Kempston Joystick mode, so you can use that method of selecting control type if you prefer.

For those who don't know, when a machine code routine is called from BASIC, the BC register pair holds the address that was called. So if you put several NOPs at the start of your routine, you have the option of calling it with a varying address that the routine can then work out by subtracting its own start address from BC. To make it easier, if your start address is an even number (for passing a 0 or 1) or a multiple of 16 for example (for passing a number 0-15, remember you'll need 15 leading NOPs at the start of your routine) you can call it with RANDOMIZE USR nnnnn+x then simply LD A,C then AND 15 to get the parameter x.

And if /when you return to BASIC, the contents of the BC register pair is turned into a number and becomes the result of the USR function. e.g. LET y = USR nnnnn will store the value of BC on return from the machine code routine in your variable y. So your machine code can return a number to BASIC. For more complex data, you could make it point to an address from where the BASIC may then read bytes of data using PEEK.
Post Reply