RISC World

ARM code for Beginners
Part 6: Passing data to and from BASIC

Brain Pickard explains how anyone can program in ARM code.

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.

  1. Write an arm routine containing a sub routine to plot two concentric circles of differing radii and colours.
    Use the left mouse button to increase the radius and the right mouse button to change the colour.

    My solution is called circles on the CD it is fully annotated. To simplify the solution I read the mouse x,y coordinates and make these the centre of the circles.

  2. Using a full descending stack write a routine in which the user enters integers which are saved on the stack. The routine should then print the numbers as it pulls them off the stack, hence showing the stack is LIFO.
    This is quite simple, my solution is called stack on the CD. The only detail worth mentioning is the integer values are stored as characters, this means printing them out is easy.

The CALL command.

The last time we mentioned this command we just called a memory location where our ARM routine started.

e.g. CALL start%

But this command can be given any number of BASIC parameters separated by commas thus:

CALL start%,integer%,real,string$,intarray%(),stringarray$()..,>

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.

  • R0 to R7 contain the values of the variables A% to H%
  • R8 points to BASIC's workspace
  • R9 points to a list (two words long for each parameter) containing details of the parameters
  • R10 contains the number of parameters

The list R9 points to has the following structure for each type of variable.

  • BASIC Syntax Type Word Address word points to
  • ?var 0 byte aligned value byte
  • !var 4 byte aligned value word
  • var% 4 word aligned value word
  • var%(n) 4 word aligned value word
  • |var 5 byte aligned value 5 bytes
  • var 5 byte aligned value 5 bytes
  • var(n) 5 byte aligned value 5 bytes
  • var$ 128 byte aligned SIB 5 bytes (see below)
  • var$(n) 128 byte aligned SIB 5 bytes (see below)
  • $var 129 byte aligned bytes ending with CHR 13
  • var%() 256+4 word aligned word pointing to array
  • var() 256+5 word aligned word pointing to array
  • 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.

     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
     LDMFD R13!,{R0-R4,PC};restore registers from stack and return
     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

     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
     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)

     LDMFD R13!,{R0-R3,R5,PC};restore regs and return
     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

     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
     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
     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)
     STMFD R13!,{R14};store register onto stack
     SWI"XOS_WriteN";prints out value R0 points to string R1= no of chrs
     LDMFD R13!,{PC};return back to param loop
     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:

     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:
     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