previous:
Tunez 2 - Asterix edition
MCCW issue 93, June-December 2000
Back to contents
next:
MEGA Guide
for those who are still scared of NestorBASIC
NestorTIPS for NestorBASIC

This article is about the extension to MSX-basic created by the author: NestorBASIC. It makes a lot of new functionality available to basic programmers. Read ahead for some useful tips.

 
Nestor Soriano Vilchez
 
Directory
NestorPreTer
NestorTIP 0: general considerations
NestorTIP 1: sharing variables
NestorTIP 2: when coordinates are bothering
NestorTIP 3: restore things to the state in which you found them
NestorTIP 4: press any space key
NestorTIP 5: BIOS can also help you
NestorEND: summarising...



About the author
I am Nestor Soriano, also known in the MSX world as Konami Man. Maybe you never heard about me, but surely you have seen some of my utility programs for MSX, which can be easily recognised because almost all of them start with the word ‘Nestor’. I am a member of the Spanish Club Mesxes and one of the makers of the magazine SD Mesxes; I am also an AAM member and therefore one of the organisers of the MSX meetings in Barcelona. Like most of you, I entered in the computer world with an MSX1 in 1986, but at first I did nothing but playing Konamis; I must admit I am a Nemesis-addict). In 1991 I acquired an MSX2+ and a couple of years after I started to learn assembler. When I changed to MSX turbo R in 1997, I had already developed most of my utilities.

The software mentioned in this article can be downloaded from my homepage: http://www.konamiman.com/.


This article is dedicated to the basic extension named NestorBASIC. Many times I hear people complaining about its excessive complexity, but the few people who use it regularly tell me that it is really useful and not so difficult once one is used to it. So with this text I would like to make you lose this fear for the alleged difficulty of programming using NestorBASIC, or at least to show you how useful can it be.

     Specifically, I will show you some examples of how NestorBASIC can be used to save basic memory, mainly with the use of one of its most powerful capabilities: the execution of machine code routines stored in mapped memory. This includes various machine code routines and basic sample listings.

NestorPreTer
But first, I need to introduce you to NestorPreTer, because all basic listings are in NestorPreTer format.

     Tired of having to remove remarks from my long basic programs when the ‘Out of memory’ messages started to appear, confused every time I found a GOSUB 10000 and did not remember what this subroutine did, and thinking about all people who had to call NestorBASIC functions via usr(x), I recently developed NestorPreTer: the MSX-basic pre-interpreter.

     Putting it shortly, we can say that while NestorBASIC (NB from now on) is a run-time help tool, NestorPreTer (NPR) is a design-time help tool. In more detail: NPR parses an input text file containing a special formatted basic program, and generates a normal MSX-basic executable — or maybe we should say ‘interpretable’ — file. This is why we call it a pre-interpreter. And what is this ‘special’ format? It is the same as the usual MSX-basic format, except that you can:

    

  • Forget line numbers — NPR will generate them automatically — and use line labels when necessary. NPR will convert labels to the appropriate line number when a branch instruction is found. For example: GOSUB ~ASK_USER or RESTORE ~MAIN_DATA.
  • Use as much comment — with the REM statement — as you want; NPR will not include it in the destination file.
  • Use macros to name constants, variables and code. For example LOCATE @ROW, @COLUMN will be converted to LOCATE X,Y; or POKE @INT_HOOK,@RET will be converted to POKE &HF349,&HC9.
  • Use indentation to make the entire program more readable.

     ...so using NestorPreTer, you make very clear, easy-to-read basic code. Isn’t that nice? Listing 1 is a NPR format code example which shows how to load NB and do some system initialisation. You can see that we create an array, named D, in which we define all the variables we will use in the program; I will explain later why we do this.

     As usual with all NestorWare, NPR is free and you can download it from my home page, see the box on the left. You better get it and read the complete manual — do not worry, it is not as long as the one of NB! — because all the listings in this article are in NPR format. Of course, also NB is available on my page.

    

Basic-listing: LIST1.ASC

'///
'/// Listing 1: NestorBASIC load / sample of NestorPreTer format code
'/// Summarising: NPR converts this listing into a normal MSX-basic file.
'///

'*********************************
'*                               *
'*  First we define some macros  *
'*                               *
'*********************************

'---  Constants  ---

@define TRUE  -1
@define FALSE  0
@define ON @TRUE
@define OFF @FALSE

'@REQ_SEGS defines the minimum number of ram segments we will need,
'including the five first ones, which are used by NB itself:
'When NB is loaded, the availability of at least REQ_SEGS segments
'is checked; if not available, an error message is prompted and
'NB is uninstalled.

@define REQ_SEGS 6

'---  Variables  ---

'We will centralise all variables in an array named D,
'which will be created after loading NB (see tip 2 for more details).
'Exception is made for loop counters, which must be simple variables.
'Also, we define simple variable @ERROR for NB functions error recovering.

@define NUM_VARS 3	'Total number of variables we will use
			'(except @ERROR and loop counters)
@define ERROR e
@define NUM_SEGS d(0)	'Total number of available segments
@define FILE_HANDLE d(1)	'Identifier for any open file
@define ANY_DATA d(2)
'...
'define here all variables you will use
'and do not forget to set NUM_VARS appropriately
'...

'We define also all the loop counters we will use
'It is better to use b, b1, b2... so we can easily initialise all
'with just a DEFINT b

@define LOOP b
@define LOOP2 b2

'---  NestorBASIC functions ---

'Using macros to name NB functions you do not need to remember
'the numbers for all functions (and vice versa, when you find
'a function call in your listing, you do not need to remember which is
'the function for that number)

@macro	NB_UNINST e=usr(0)	'Uninstalls NestorBASIC
@macro	NB_INFO e=usr(1)	'Gets info about NestorBASIC
@macro	R_SEG	e=usr(2)	'Reads a byte from a segment

'...
'define here all functions you will use, or define all
'in an external macros file (this is better).
'In my HP you can find a file with all NB functions defined as macros.
'...

'---   Samples of useful macros  ---

'Clear keyboard buffer:

@macro	CLEAR_KEY_BUFFER defusr1=&h156: @ERROR=usr1(0)

'Check if ENTER is being pressed (if @PRESS_ENTER then...)

@macro	PRESS_ENTER (peek(&HFBEC) and 128) = 0

'Uninstalls NB freeing basic memory, and ends.
'BEWARE: Do not uninstall NB from inside a turbo block!

@macro	FINISH	p(0)=@YES: @NB_UNINST: end

'--- To identify the listing ---

@remon

'list1.bas

@remoff


'**********************
'*                    *
'*  NestorBASIC load  *
'*                    *
'**********************

~	maxfiles=0:			'This saves about 250 bytes
	keyoff: screen 0: width 80:
	color: color 15,0,0:
	?"--- Loading NestorBASIC... ---": ?:
	bload"nbasic.bin",r:
	defint @ERROR:		'NB error variable
	@ERROR= 0:
	if p(0) >4 then
	      goto ~LOADOK	'No error if P(0) is at least 5
	else
		?"ERROR: ";

'*** Error? Then show message and finish.

~	if p(0)=0 then
		?"No mapped memory or only 64K found!":
		end

~	if p(0)=1 then
		?"Disk error when loading NestorBASIC!":
		end

~	if p(0)=2 then
		?"No free segments!":
		end

~	if p(0)=3 then
		?"NestorBASIC was already installed.":
		@NB_INFO:
		goto ~ALR_LOAD

~	if p(0)=4 then
		?"Unknown error.":
		end

'*** Jumps here if NestorBASIC loaded successfully.

~LOADOK:	?"NestorBASIC loaded successfully!":
		?"Available segments:"; p(0): ?

'*** Jumps here if NestorBASIC was already loaded.

~ALR_LOAD

'*** Checks if we have at least REQ_SEGS segments, else ends.

~	defint d: dim d(@NUM_VARS):	'Creates variables array
	if p(0)< @REQ_SEGS then
		?"ERROR: Not enough free segments!":
		?"I have"; p(0) ;" segments and I need at least";
		@REQ_SEGS; "segments.":
		@FINISH
	else
		@NUM_SEGS= p(0)
'...
'Put your program from here. Suggestion for the beginning:

_turbo on (p(), d(), e)
dim f$(1): defint @LOOP: @LOOP=0: @LOOP2=0

NestorTIP 0: general considerations
Imagine that you are an assembler programmer — I suppose you actually are — and you need to make an editor for a game you are developing, with maps, sprites, etc. For such a purpose the most practical way is to use basic, because you do not have a real need for speed and coding it in machine code directly could take you as much time as the game itself.

     But soon you bump into the eternal problem of basic programs: the lack of memory. We have a 128, 256, even 1024 or 4096 kB machine, but we can use only 23 kB, and if we use Turbo-basic it is even worse: just 10 kB!

     NB solves this problem partially: we cannot make bigger programs, but we can use mapped memory to store data. So from now on do not forget this: do not store in your basic program whatever you can store in mapped memory! So from now on it is ‘forbidden’ — you choose the prohibition level — to put in your programs:

  • Strings. It is very easy to spend, without realizing it, 3 or 4 kB with just PRINT "Welcome to my amazing program which is the best one in the world", PRINT "Please enter the name of the desired filename" and so on.
  • Data for coordinates, key combinations, etc... For example “IF X>34 AND X<100 THEN 1000 ELSE IF X<34 THEN...”
  • DATAs. Making a loop for reading data from mapped memory is almost as easy as making a READ loop.

     We can also save basic memory — and gain speed as a side effect — if we perform some tasks in assembler. For example, read the mouse and cursor keys and then check if the pointer does not move beyond the screen limits. Of course these assembler routines will also be placed in mapped memory so no basic memory is spent.

     And maybe you think ‘I also save memory if I do not use comments’. If you use NestorPreTer you do not need to worry about this, otherwise you must of course minimise the number of comments and their length in order to save memory.

     Last but not least: now that you will use mapped memory, take paper and pencil and draw a map of all the segments, i.e., what you will put in them and on what address. This will make the further coding process much easier. Years of experience have taught me this.

     And after these introductory words, let us start with some more detailed tips.

NestorTIP 1: sharing variables
This is not exactly a tip for saving memory, but it is also very useful.

     You already know, I hope, that NB has functions for storing basic programs in ram segments, and to execute them without losing current variables. But maybe for any reason you cannot — due to a lack of segments, for example — or do not want to use these functions. Well then, we are not lost yet: there is a way to load another basic program in the usual way — RUN"PROGRAM" — without losing variables. How? This is what I will explain now.

     The tip is just to save all variables in mapped memory, then load the new program, and then recover the variables. It sounds easy... and actually it is, if we have all variables in one single array; that is why we define array D in listing 1, as I explained in the introduction text.

     More detailed explanation: if we put all variables in one array, then all of them are stored consecutively in memory and therefore we can copy all of them easily to any ram segment, using the NB function for memory block transfers, which is function number 10. But how do we know where the array is stored in basic memory? That is what the basic instruction VARPTR is for! Add the fact that basic memory has segment number 255 assigned when using NB functions and we have listing 2a.

     Once we have saved all variables, we load the new program with a common LOAD, we create again array D, we recover variables with the same NB function, swapping source and destination parameters and we can continue our task. See listing 2b.

    

Basic-listing: LIST2A

'///
'/// Listing 2a: storing all variables (array D) in a mapped memory segment.
'/// Execute it before loading another basic program.
'/// We suppose that all variables are of integer type.
'///

@macro LDIRSS e=usr(10)        'Memory block transfer function

'Segment and address where variables will be saved:
'define it as you want, I just use example values.

@define DATA_SEG 6
@define DATA_DIR &H100

'Now we save variables. We suppose that NUM_VARS is appropriately defined,
'for example with listing 1.

~	p(0)= 255:	'basic memory segment number
	p(1)= varptr(d(0)):	'Address in basic memory of the array
	p(2)= @DATA_SEG: 'Destination for transfer
	p(3)= @DATA_DIR:
	p(4)= @NUM_VARS * 2:	'Block size: each integer var = 2 bytes
	p(5)= @NO:	'Or @YES, you set it as you want
	p(6)= @NO:	'Idem
	@LDIRSS:

	run "nextprog.bas"        'Now we can execute another basic program

Basic-listing: LIST2B

'///
'/// Listing 2b: Recovering the variables previously saved with listing 2a.
'/// Execute it at the beginning of the program which must recover
'/// the variables from the "parent" program.
'///

@remon
'nextprog.bas
@remoff

@macro LDIRSS e=usr(10)        'Memory block transfer function

'Segment an address where variables are saved.
'Again you can define it as you want, but of course you must use
'the same values used in listing 2a!
'And again, we suppose @NUM_VARS already defined

@define DATA_SEG 6
@define DATA_DIR &H100

'First we define array P again so we can use NB functions.

~	defint p: dim p(15): define @ERROR: @ERROR=0:

'Now we create again data array D. Again, we suppose
'@NUM_VARS already defined.
'Suggestion: define DATA_SEG, DATA_DIR and NUM_VARS in an external macros
'file, and use this file when processing with NPR both parent and child basic
'programs.

	defint d: dim d(@NUM_VARS):

'Now we have D again, we recover variables:

	p(0)= @DATA_SEG:
	p(1)= @DATA_DIR:
	p(2)= 255:
	p(3)= varptr(d(0)):
	p(4)= @NUM_VARS * 2:
	p(5)= @NO:
	p(6)= @NO:
	@LDIRSS

'...and from here, life goes on:

_turbo on (p(), d(), e)
dim f$(1): defint @BUCLE: @BUCLE=0: @BUCLE2=0

'etc...

NestorTIP 2: when coordinates are bothering
Let us continue with the example of the editor for the game. Surely it will have a graphical environment including icons and mouse control. Then, when a mouse button click is detected, you must find out which icon the pointer is pointing at. How do you do this?

     The normal way — actually I cannot think of another — is to have a table with start and end coordinates for each icon and with a given current pointer coordinates, we scan the table checking if these current coordinates are or are not inside the coordinates range for each icon. That is, if an icon is located from (X0,Y0) to (X1,Y1), we must check if X0<X<X1 and Y0<Y<Y1. Simple and easy.

     Of course you can do this in plain basic, storing the table of coordinates in a DATA line. But if we use NestorBASIC we can use assembler, which is faster, easier and consumes less memory.

     Let us see how. In a ram segment we will save an assembler routine, together with the coordinates table for all the icons we have on the screen. When calling this routine with function 59, we just pass current mouse coordinates as parameters, then the table will be scanned, and we get the icon number as a result. It saves an incredible amount of memory and we also gain speed.

     So here you have three more listings. Listing 3 is a universal routine for loading a file into a segment; we will use it to load the ML routine and the icons table. I will also use it in the other tips. Listing 4a is the table scanner in assembler and listing 4b is some basic sample code which calls the assembler routine previously loaded.

    

Basic-listing: LIST3

'///
'/// Listing 3: Loading a entire file in a segment
'/// (file must have a maximum length of 16K, of course)
'/// NOTE: @ERROR and @FILE_HANDLE are defined in listing 1
'///

@macro	F_OPEN	e=usr(31)	'Open a file
@macro	F_CLOSE e=usr(32)	'Close a file
@macro	F_READ	e=usr(33)	'Read from a file

'Segment and address where we will load file, define it as you want.
'FILE_SIZE is the amount of bytes we will try to read from the file.

@define FILE_SEG 6
@define FILE_DIR 0
@define FILE_SIZE 16384-@FILE_DIR

'Name and path for file to be loaded

@define FILENAME "c:\routines\anything.bin"

'We open file, if error, we jump to a routine ~ERROR,
'which we suppose defined anywhere.

~List_3:
	F$(0)= @FILENAME:
	@F_OPEN:
	if @ERROR<>0 then ~ERROR else
	@FILE_HANDLE= p(0)

'Now we try to read 16K from file. If we obtain error 1 or 199 (for DOS 1 and 
'DOS 2 respectively) we ignore it, because this error means just "End of 
'file", that is, file is smaller than 16K

~	p(0)= @FILE_HANDLE:
	p(2)= @FILE_SEG:
	p(3)= @FILE_DIR:
	p(4)= @FILE_SIZE:
	p(6)= @NO:
	@F_READ:
	if (@ERROR<>0 and @ERROR<>1 and @ERROR<>199) then ~ERROR

'All done, now we just close the file.

~	p(0)= @FILE_HANDLE:
	@F_CLOSE:
	return

ML-listing: LIST4A

;--- Listing 4a: Icon scanner in assembler
;    With a given pair of coordinates, it scans a coordinates table
;    corresponding to icons positions, and it returns the icon number
;    which contains the given coordinates.
;    Input:   P(3) = BC = X coordinate
;	      P(4) = DE = Y coordinate
;    Output:  P(5) = HL = Found icon identifier (-1:none)

	org	#8000

	push	bc,de	;So P(3) and P(4) are not modified
	call	CHKICON
	pop	de,bc
	ret

CHKICON:	ld	ix,TABLE_ICON

LOOPICON:	 ld	 a,(ix)
	cp	#FF
	jr	z,ENDICON

	ld	l,(ix+1)
	ld	h,(ix+3)
	ld	a,c
	call	RANGE
	jr	nz,NOICON

	ld	l,(ix+2)
	ld	h,(ix+4)
	ld	a,e
	call	RANGE
	jr	nz,NOICON

YESICON: ld	 l,(ix)
	ld	h,0
	ret

NOICON:	inc	ix
	inc	ix
	inc	ix
	inc	ix
	inc	ix
	jr	LOOPICON

ENDICON:	ld	hl,-1
	ret

;--- Subroutine: RANGE
;      Checks a byte for being or not inside a given range
;    INPUT:	 H = Upper value of range (inclusive)
;		 L = Lower value of range (inclusive)
;                A = Byte
;    OUTPUT:	 Z = 1 if inside range (Cy = ?)
;		 Cy= 1 if above range (Z = 0)
;		 Cy= 0 if below range (Z = 0)
;    MODIFY:	 AF

RANGE:	cp	l	;Smaller?
	ccf
	ret	nc

	cp	h	;Bigger?
	jr	z,R_H
	ccf
	ret	c

R_H:	push	bc	;=H?
	ld	b,a
	xor	a
	ld	a,b
	pop	bc
	ret


;--- Icons coordinates table
;    Format: identifier + start x + start y + end x + end y
;    (1 byte each one)

;    NOTE: identifier #FF is reserved for the end of table mark (mandatory)

;    Assuming that all icons are placed in a rectangular array starting with
;    base position (BX, BY), and icons have a size TX x TY, we can use
;    the macro "icon":

BX:	equ	0	;Sample values
BY:	equ	200
TX:	equ	11
TY:	equ	11

icon:	macro	@num,@xi,@yi
	db	@num,BX+@xi*TX,BY+@yi*TY,BX+@xi*TX+TX-1,BY+@yi*TY+TY-1
	endm

;Sample: 5 x 2 icons table:
;01234
;56789

TABLE_ICON:
_0:	icon	0,0,0
_1:	icon	1,1,0
_2:	icon	2,2,0
_3:	icon	3,3,0
_4:	icon	4,4,0
_5:	icon	5,0,1
_6:	icon	6,1,1
_7:	icon	7,2,1
_8:	icon	8,3,1
_9:	icon	9,4,1
END:	db	#FF

Basic-listing: LIST4B

'///
'/// Listing 4b: Sample of the use of the icons scanner from NestorBASIC,
'/// in a short, useful, fast and elegant way. (-v-)v
'///

@macro	ASSEMB_EXE  e=usr(59)

'First we load the file containing the routine in any segment.
'NOTE: Load address (@FILE_DIR) must be the same where the routine has been 
'assembled (ORG directive on listing 4a):

@define ORG_DIR &H8000

@define FILE_SEG 6
@define FILE_DIR @ORG_DIR
@define FILE_SIZE 16384-(@ORG_DIR-&H8000)

~	gosub ~List_3

'...
'... Any code in which, for example, we detect a mouse click
'... in coordinates (X,Y)
'...

'Now we get the clicked icon number depending of the coordinates:

~	p(3)= x:
	p(4)= y:
	gosub ~CALL_ASSEMMB:
	if p(5)= -1 then ~NO_ICON
	else on p(5) gosub... 'Or: goto ~PROCESS_ICON

'Subroutine for calling the assembler routine

~CALL_ASSEMB:
	p(0)= @FILE_SEG:
	p(1)= @FILE_DIR:
	@ASSEMB_EXE:
	return

NestorTIP 3: restore things to the state in which you found them
This is not a very impressing tip and it does not use assembler routines at all, but it can be useful.

     Let us suppose your program is using text mode. It sets SCREEN 0, WIDTH 80 and black and white colours. It also performs a KEY OFF and switches off the key click sound. You do all of this at the beginning of the program and it works, but... what happens when your program finishes? The screen state remains as you have set it, that is, the initial state is not restored.

     Solution: save the screen’s initial state, which we will get by looking at some system variables, before setting the desired screen mode (listing 5a). Then, when our program finishes, we can retrieve these initial settings (listing 5b). And of course, we will save this state in mapped memory, so no single byte of basic memory is lost.

    

Basic-listing: LIST5A

'///
'/// Listing 5a: saving screen state in a ram segment
'///

@macro	R_SEGI	e=usr(3)	'Reading from a segment with auto increment
@macro	WSEGI	e=usr(7)	'Idem for writing

'Segment and address where we will save state
'As usual, you must define it as you want

@define STA_SEG 6
@define STA_DIR 0

@define VARIABLE v	'For a loop

'System variables we will save

@define LINLEN &HF3B0	'Current WIDTH
@define CRTCNT &HF3B1	'Number of lines on screen
@define CLIKSW &HF3DB	'Key click sound, (0=no, other=yes)
@define CNSDFG &HF3DE	'KEY ON (0) / OFF (other)
@define FORCLR &HF3E9	'Text color
@define BAKCLR &HF3EA	'Back color
@define BDRCLR &HF3EB	'Border color
@define SCRMOD &HFCAF	'Current SCREEN

'We save state with a simple loop
'(OK, I said before that it is better to not use DATAs in order to save 
'memory... but this is just a sample! Besides you surely will put this code 
'in a short initialisation program rather than in the main program)

~	p(0)= STA_SEG:
	p(1)= STA_DIR:
	restore ~VARIABLES

~	read @VARIABLE:
	if @VARIABLE<>0 then
	p(2)= peek (@VARIABLE):
	@W_SEGI:
	goto ~~

~DONE: 'Here life goes on...

..

'Here you have the used system variables, be careful to not modify
'the variables order in the DATA line, else listing 5b will not work!

~VARIABLES:
	data	@SCRMOD, @LINLEN, @CRTCNT, @CLIKSW, @CNSDFG,
		@FORCLR, @BAKCLR, @BORCLR, 0

Basic-listing: LIST5B.ASC

'///
'/// Listing 5b: Restoring initial state, saved with listing 5a
'/// NOTE: execute this code out of turbo blocks in order to avoid
'/// problems with some incompatible instructions
'///

~	p(0)= STA_SEG:
	p(1)= STA_DIR:

	@R_SEGI:	'SCREEN
	screen p(2):

	@R_SEGI:	'WIDTH
	width p(2):

	@R_SEGI:	'Lines in screen
	poke @CRTCNT, p(2):

	@R_SEGI:	'Key sound
	if p(2)=0 then screen ,,0
	else screen ,,1

~	@R_SEGI:	'KEY ON/OFF
	if p(2)=0 then keyon
	else keyoff

~	@R_SEGI:	'Colours
	color p(2):
	@R_SEGI:
	color ,p(2):
	@R_SEGI:
	color ,,p(2)

NestorTIP 4: press any space key
This tip is similar to tip 2. If we use assembler to detect icons, why we cannot we do same to detect key pressing? The idea is simple: instead of using INKEY$, INPUT$ and similar basic instructions, we save in any segment a table with the keys we are interested in plus an assembler routine which will detect what key or key combination is being pressed, returning its associated identifier.

     Advantages: as in the case of the icons scanner, we save basic memory — one single USR is enough to scan all the keys in the table and detect the key/combination being pressed — get more speed and make it easy to detect ‘difficult’ keys such as SHIFT, CTRL, SELECT, ESC...

     Maybe you are asking yourself how a key table can be stored. Well, as you may already know, when working in assembler, the keyboard is seen as an 11×8 array, in which every key has a row number and a column number assigned: see Figure 1.

    

Figure 1: Row and column number for each key
 7: 6: 5: 4: 3: 2: 1: 0:
0: 7 6 5 4 3 2 1 0
1: ; ] [ \ = - 9 8
2: B A ACCENT / . ,
3: J I H G F E D C
4: R Q P O N M L K
5: Z Y X W V U T S
6: F3 F2 F1 CODE CAPS GRPH CTRL SHIFT
7: RET SEL BS STOP TAB ESC F5 F4
8: RIGHT DOWN UP LEFT DEL INS HOME SPACE
9: 4 3 2 1 0 / + *
10: . , - 9 8 7 6 5

     The assembler routine we will use (list 6a) in order to simplify things, detects just the pressed keys, or keys pressed together with SHIFT and/or CTRL. Therefore, for each key we have the following in the table: an identifier, SHIFT/CTRL required combination, row and column. Apart from this, the operating system itself scans the keyboard at each clock interrupt (50 or 60 Hz) and stores information about the status for every key in the system’s work area. So, we just need to read this system area zone in order to know if a given key is being pressed or not.

     Last but not least: in addition to the key or key combination currently being pressed, this routine also returns the key or key combination that was being pressed in the previous call. This is very useful to detect a key press only once, even if the key is still pressed: we consider the key pressed only if it is pressed currently and it was not pressed in the previous call. Alternatively, we can also detect a key release: a key is not being pressed but it was pressed in the previous call.

     Listing 6b is a basic sample of the use of the routines of listing 6a.

    

ML-listing: LIST6A

;--- Listing 6a: Checks if any key of the keys table is pressed
;    Input:   -
;    Output:  BC = P(3) = Key/combination currently pressed (-1 = none)
;	      DE = P(4) = K/C pressed in the previous call

	org	#8000

;NEWKEY is the system work area where keyboard status is saved by OS.
;It is 11 bytes long, each byte is a row, and for each byte, each bit
;is the state for the corresponding column (1=not pressed, 0=pressed)

NEWKEY: equ	#FBE5

CHKEY:	ld	ix,TABKEY

LOPKEY: ld	a,(ix)
	cp	#FF
	jr	z,NOKEY

	ld	e,(ix+2)
	ld	d,0
	ld	hl,NEWKEY
	add	hl,de
	ld	a,(hl)	;A = Row state
	cpl

	ld	b,(ix+3)
	inc	b
LOPFIL: srl	a
	djnz	LOPFIL
	jr	nc,NEXTKEY	;Cy = Key state (1=Pressed)

	ld	a,(NEWKEY+6)
	cpl
	and	3
	cp	(ix+1)
	jr	nz,NEXTKEY	;Checks for SHIFT and CTRL

OKKEY:	ld	c,(ix)	;Combination pressed?
	ld	b,0
	ld	de,(OLDKEY)
	ld	(OLDKEY),bc
	ret

NEXTKEY:	inc	ix	;Next combination
	inc	ix
	inc	ix
	inc	ix
	jr	LOPKEY

NOKEY:	ld	bc,-1	;No key or combination pressed
	ld	de,(OLDKEY)
	ld	(OLDKEY),bc
	ret

OLDKEY: dw	-1	;For storing previous combination

;--- Keys table
;    Format: identifier + SHICT + row + column
;    (1 byte each one)
;    SHICT = &B000000CS
;    C=1 if CTRL pressed is required
;    S=1 if SHIFT pressed is required
;    In other words:
;    SHICT=0 for key alone
;    SHICT=1 for key + SHIFT
;    SHICT=2 for key + CTRL
;    SHICT=3 for key + SHIFT + CTRL
;    See figure 1 for the corresponding row and column for each key

;NOTE: identifier #FF is reserved for end of table mark (mandatory)

TABKEY: ;Example table
	db	0,0,7,2	;ESC
	db	1,2,7,2	;CTRL+ESC
	db	2,3,7,2	;CTRL+SHIFT+ESC
	db	3,1,7,2	;SHIFT+ESC
	db	4,0,6,5	;F1
	db	5,3,6,3	;CTRL+SHIFT+CAPS
	db	#FF

Basic-listing: LIST6B

'///
'/// Listing 6b: Detects if any of the keys on the example table in listing 6a
'/// is being pressed (pressing and releasing is detected)
'///

@macro	ASSEMB_EXE  e=usr(59)

'First we load the file with the routine and the table.
'See note about ORG_DIR in listing 4a

@define ORG_DIR &H8000

@define FILE_SEG 6
@define FILE_DIR @ORG_DIR
@define FILE_SIZE 16384-(@ORG_DIR-&H8000)

~	gosub ~List_3

'Some initialisation

~	screen 0,,0:
	width 40:
	color 15,0,0:
	keyoff

'We create a strings array to show information about the key being pressed.
'OK, OK, I said you "do not store strings in the basic program", but...
'THIS IS JUST A SAMPLE!!

@define STRINGS c$

~	dim @STRINGS(6):
	@STRINGS(0)= "ESC":
	@STRINGS(1)= "CTRL+ESC":
	@STRINGS(2)= "CTRL+SHIFT+ESC":
	@STRINGS(3)= "SHIFT+ESC":
	@STRINGS(4)= "F1":
	@STRINGS(5)= "CTRL+SHIFT+CAPS"

'Infinite loop for detect keys and show which one is pressed

@macro	BEEPEA	beep:beep:beep

~INFINITE:
	gosub ~CALL_ASSEMB:

	if p(3)=-1 and p(4)<>-1 then	'A key is released?
	?"Release ";@STRINGS( p(4) );" !!": @BEEPEA else

	if p(3)<>-1 and p(4)=-1 then	'A key is pressed?
	? @STRINGS( p(3) ): @BEEPEA

~	cls:
	goto ~INFINITE

'Subroutine for calling the assembler routine

~CALL_ASSEMB:
	p(0)= @FILE_SEG:
	p(1)= @FILE_DIR:
	@ASSEMB_EXE:
	return

NestorTIP 5: BIOS can also help you
We have spoken a lot about the use of our own assembler routines, but we forgot that we have also a good set of ready-to-use routines in the ROM of our machines: the BIOS. Of course you can use the BIOS via the (DEF)USR instruction; you do not need NestorBASIC at all. However, if you use USR you cannot set the input registers, nor look at the output registers, while if you use NestorBASIC function 58 you can.

     Most of the BIOS routines are not more than assembler versions of basic instructions, but there are some that will be very useful for us. For example CHGCPU (&H0180) and GETCPU (&H1083), which respectively change and get the current processor on the Turbo-R. See listing 7.

    

Basic-listing: LIST7

'///
'/// Listing 7: Getting and setting the processor on the Turbo-R using BIOS
'///

@macro	BIOS	e=usr(58)	'Function to execute BIOS routines

@define GETCPU	&H180
@define SETCPU	&H183
@define Z80	0	'CPU modes
@define R800ROM 1
@define R800DRAM	2

'MSXVER= MSX version: 0=MSX1, 1=MSX2, 2=MSX2+, 3=MSX Turbo-R

@macro	MSXVER	peek (&H2D)

'Getting current CPU: after calling GETCPU, we have AF in p(2); current
mode is stored in A, therefore:

~	if @MSXVER<3 then ~NO_TURBOR else
	p(0)= 0:
	p(1)= @GETCPU:
	@BIOS:

	p(2)= (p(2)\256) and 255: 'Note: integer division (inv. bar or yen)
	if p(2)= @Z80 then...
	else if p(2)= @R800ROM then...
	else...

'Setting CPU: to set 0, 1 or 2 in A, we put &H8000, &H8100 or &H8200 in AF, 
'that is, in p(2). The 8 is to update the processor led appropriately; if we 
'change it into a 0, led will not change.

~	if @MSXVER&lt;3 then ~NO_TURBOR else
	p(2)= @R800ROM: 'or any other
	p(2)= p(2)*256:
	p(2)= p(2) or &H8000:	'If we want led update

	p(0)= 0:
	p(1)= @SETCPU:
	@BIOS

     More examples: CHGCAP (&H0132) allows you to change the CAPS led state (see listing 8). Or GETPLT (&H0149) in the subrom, which allows you to get the palette data for a given color number (list 9). As you can see there are a lot of possibilities: it is up to you to scan the BIOS routines listing in order to find the routine you were searching for.

    

Basic-listing: LIST8

'///
'/// Listing 8: Changing CAPS led state with CHGCAP
'/// Use: set value 0 (led ON) or any other (led OFF) before calling CHGCAP
'///

@define OFF	1
@define ON	0

@define BIOS	e=usr(58)
@define CHGCAP	&H132

~	p(2)= @ON*256:	     'or @OFF
	p(0)= 0:
	p(1)= @CHGCAP:
	@BIOS

Basic-listing: LIST9

'///
'/// Listing 9: Getting palette data information with GETPLT
'///

@define COLOR c
@define RED r
@define GREEN g
@define BLUE b

@define BIOS	e=usr(58)
@define GETPLT	&H149

'GETPLT works in the following way:
'Input:  A = Color to get information for (p(2)=AF)
'Output: BC = p(4) = GREEN + 256*BLUE + 4096*RED

~	@COLOR= 7	'Sample color
	p(0)= 1:	'Now we call SUB-BIOS
	p(1)= @GETPLT:
	p(2)= @COLOR*256:
	@BIOS:

	@RED= (p(3)\4096) and 15:
	@GREEN= p(3) and 15:
	@BLUE= (p(3)\256) and 15:
	print @COLOR; "="; @RED, @GREEN, @BLUE

NestorEND: summarising...
Basic programs are easy to make, but slow and very limited; assembler programs are a lot more powerful, but even the simplest task becomes a very time consuming and complicated thing. NestorBASIC wants to be an intermediate solution, as well as a good alternative to the standard hybrid programming: placing your assembler routines in mapped memory enables you to save a lot of the always scarce basic memory and also you can easily set and read all the registers.

     Although probably I will not continue the development of NestorBASIC, I continue developing assembler extensions and I have noticed other people are doing the same. Remember to visit my home page from time to time and if you have any doubt or suggestion, do not hesitate to mail me.

     I hope you find these tips useful. Thanks for your time and... enjoy programming!

previous:
Tunez 2 - Asterix edition
MSX Computer & Club Webmagazine
issue 93, June-December 2000
next:
MEGA Guide