So far we have only passed integers from a BASIC program via the variables A% to H% which are given to registers R0 to R7 and back via the USR command using register R0. How can we send strings, real numbers and data in BASIC variables? In this part I will show how the CALL command can be used to pass data from a BASIC program to our ARM routines. But first the solutions to problems set in part 5.
The last time we mentioned this command we just called a memory location where our ARM routine started.
But this command can be given any number of BASIC parameters separated by commas thus:
Any BASIC variable type may be used including $string%, memory%!word%, memory?byte%. This means we can send data from BASIC programs to ARM routines, work on it then return to our BASIC program.
Reading the Variables
When the CALL command is used with variables the following registers are used by RISCOS.
The list R9 points to has the following structure for each type of variable.
var$() 256+128 word aligned word pointing to array
This table looks rather complicated but it shows some important information.
Some of the values the address word points to are byte aligned which makes the ARM code a little more involved. The type values are quite logical:
Values less than 128 are for single value parameters and the address word points to the value. Also the type value can be used to detect the length of memory used for the value. For values with bit 7 set (128) the address word points to a String Information Block (SIB) which has its first four bytes pointing to the first character of the string and the next byte containing the length of the string. Values with bit 8 set (256) show the parameter is an array. I will return to these in the next part. Sometimes it is easier to visualise the above using a diagram thus:
For the string type variable text$
Reading parameter values
1. CALL read%,?var - This is the simplest parameter.
[
.read%
STMFD R13!,{R0-R4,R14};store registers to stack
LDR R3,[R9,#0];pointer to mem loc
LDR R4,[R9,#4];type (=0)
LDRB R0,[R3,#0];r0 now contains value
ADR R1,buffer%;r1 points to buffer for conversion
MOV R2,#8;length of buffer
SWI"XOS_ConvertInteger3";changes integer to string for printing
SWI"XOS_Write0";prints out value
SWI"XOS_NewLine";newline
LDMFD R13!,{R0-R4,PC};restore registers from stack and return
.buffer%
EQUD 0
EQUD 0
]
All non string parameters have to be converted to strings before SWI"OS_Write0" can print out their values. SWI"OS_ConvertInteger3" does this and conveniently returns with R0 pointing to the buffer containing the string, ready for OS_Write0 to print. This example is on the CD and is called ?var.
Now lets add another type of parameter
CALL read%,?var%,var%!4
[
.read%
STMFD R13!,{R0-R5,R14};store registers to stack
MOV R5,R10,LSL#3;R5 now has number of parameters*8 (=2*8)
.paramloop%;start of the parameter loop
SUB R5,R5,#8;subtract 8 from R5 is this the last parameter?
BL readparam%;branch to reading parameter sub routine
CMP R5,#0;have we reached the end of the param block
BGT paramloop%;if not branch back for next
LDMFD R13!,{R0-R5,PC};restore registers and return
.readparam%
STMFD R13!,{R0-R3,R5,R14};store registers on stack
LDR R3,[R9,R5];r3 holds address of parameter memory block
ADD R5,R5,#4;add 4 to R5
LDR R4,[R9,R5];r4 = type number of the parameter
CMP R4,#0;is this a type=0 parameter
BLEQ single_byte_value%if so then branch to its sub routine
BEQ endread%;and branch to end of sub routine
CMP R4,#4;is this a type=4 parameter
BLEQ integer_value%;if so then branch to subroutine to read value
BEQ endread%
;more conditions to be added here (see later)
.endread%
LDMFD R13!,{R0-R3,R5,PC};restore regs and return
.byte_aligned_value%
STMFD R13!,{R1-R3,R14};store registers on stack
LDRB R0,[R3],#1;get 1st byte
LDRB R1,[R3],#1;get 2nd byte
ORR R0,R0,R1,LSL #8;building up the word length value in R0
LDRB R1,[R3],#1;get 3rd byte
ORR R0,R0,R1,LSL #16;building up the word length value in R0
LDRB R1,[R3],#1;get last byte
ORR R0,R0,R1,LSL #24;building up the word length value in R0
LDMFD R13!,{R1-R3,PC};restore registers and return
.single_byte_value%
STMFD R13!,{R14};store register onto stack
LDRB R0,[R3,#0];load R0 with the single byte value
BL integer_to_string%;convert ready for printing out
BL printparamval%;branch to printing the value sub routine
LDMFD R13!,{PC};restore register and return
.integer_to_string%
STMFD R13!,{R14};store return address on stack
ADR R1,buffer%;R1 points to buffer for conversion
MOV R2,#8;length of buffer
SWI"XOS_ConvertInteger3";produces string copy of integer
RSB R1,R2,#8;R1=8-R2 ie length of string
LDMFD R13!,{PC};restore PC ie return
.integer_value%
STMFD R13!,{R14};store register onto stack
BL byte_aligned_value%;load R0 with contents of byte aligned word
BL integer_to_string%;convert integer to string for printing
BL printparamval%;branch to printing the value sub routine
LDMFD R13!,{PC};restore register and return
;more reading value subroutines go here (see later)
.printparamval%
STMFD R13!,{R14};store register onto stack
SWI"XOS_WriteN";prints out value R0 points to string R1= no of chrs
SWI"XOS_NewLine";newline
LDMFD R13!,{PC};return back to param loop
.buffer%
EQUD 0
EQUD 0
EQUD 0
]
As you can see I have restructured the routine and used sub routines. SWI OS_WriteN is used for printing as this can easily be used for all types of parameters.
The above coding can be used as a template. Any extra routines I describe can be placed in the positions marked. The byte_aligned_value subroutine looks complicated but is required since the memory address word containing the integer value does not start on a word boundary. The ORR R0,R0,R1,LSL#n command builds up the register R0 with the value. The LSL (logical shift left) applies to the register R1 and is executed first (remember the barrel shifter from part 1?). The 'ORR' is then executed. Another example of the elegance of the ARM.
The integer_to_string subroutine converts an integer to a string ready for printing using SWI"OS_WriteN". This example is called 2params on the CDROM. You might have noticed the order in which the above routine prints the variable values is from left to right. The list held in R9 is last in first out so the routine reads the list backwards.
Now for the string parameters
CALL read%, ?var%, var%!4, text$
The text$ parameter will have a type 128 and the data is a byte aligned 5 byte SIB block, containing the pointer to the actual text and the length byte. The code to get R0 to point to the text and R1 to equal the length is as follows:
.get_text_from_SIB%
STMFD R13!,{R14};store registers on stack
BL byte_aligned_value%;load R0 with pointer to actual text
LDRB R1,[R3,#4];load R1 with length byte
LDMFD R13!,{PC};restore register and return
This routine can be placed after the byte_aligned_value% subroutine. In readparam% sub routine extra code is required to call get_text_from_SIB% when the type is 128.
CMP R4,#128;is this a string
BLEQ get_text_from_SIB%;if so then branch to get data from SIB
BLEQ printparamval%;branch to printout routine
BEQ endread%;this line here for later additions to routine
This is placed after the last BEQ endread%. This full routine is called 3params on the CD. At this stage we can now read the values for single byte, integer and string parameters.
The last type of variable I will include in this part has the BASIC syntax $string%. This has type 129 and the address points to byte aligned bytes ending with ASCII 13.
CMP R4,#129;is this a $string% param
BLEQ direct_string_value%;if so branch to its sub routine
BEQ endread%;again this added for later additions
This goes after the last BEQ endread% in the readparam% sub routine
The code to get R0 to point to the text is as follows:
.direct_string_value%
STMFD R13,{R2,R14};store registers on stack
MOV R0,R3;copy start address of string into R0
MOV R1,#0;cancel R1 to zero for start of loop
.stringlengthloop%;enter length counting loop
LDRB R2,[R0,R1];load chr code into R2 (byte load)
CMP R2,#13;is this code = 13?
ADDNE R1,R1,#1;if not then add one to counter
BNE stringlengthloop%;if not then go back and get next chr
BL printparamval%;branch to printout routine
LDMFD R13!,{R2,PC};restore registers and return, R1=length of string
The direct_string_value% sub routine counts till it finds the ASCII 13 and places the length in R1 so that the same printparamval% routine can be used.
This code can go after the get_text_from_SIB% subroutine. The full routine is called 4params on the CD.
Now that we have catered for all the parameter types a short description explaining the design of the routine.
Structured Approach
The routine is broken down into small sub routines to make it easily extendable. Note the use of the full descending stack to return from each sub routine and to restore registers. This technique allows registers to become local within their sub routines.
Looking at the byte_aligned_value% routine. This uses R0 to R3. R0 is not pushed on the stack so this is not local (it holds the contents of the byte aligned word). R1 to R3 are pushed on the stack so they are local since their values are restored (pulled off the stack) when returning from the sub routine.
Altering Parameter Values
The next step is being able to work on the values of variables. The first routine I will describe is to swap the values of two parameters of the same type.
To do this we DO NOT change the data but swap the data addresses for the parameters.
Consider the following
CALL swap%,string1$,string2$
The routine swap% must find the data addresses of both parameters then update each SIB with data address of the other.
The full annotated listing is within the program called swap on the CD. You might say 'so what' we can do this using the SWAP command in BASIC! But try using this program with more than two strings!
A good exercise is to see how the stack changes as the routine runs and hence understand how the swap occurs. Thats all for this time, next time I will extend our template program to include integer and string arrays and use this technique to change their contents and search for values.
Problems to solve
1. Extend the swap program to allow integers of type 4 to be swapped. Remember to trap for a mixture of types of parameter. If this is found just leave the parameters unchanged.
2. Write a program that takes two params, a string and a type 0 byte. The byte value to be used as an index into the string. Print out the character.
Brain Pickard