RISC World

ARM code for Beginners
Part 4: More on SWI routines and Memory useage.

Brain Pickard explains how anyone can program in ARM code.

Solutions to Part 3 problems.

Show the BASIC programming listing to produce a two pass assembler which does not check errors on the first pass and never produces a listing. When would these settings be used?

The values for OPT is 0 (zero) and 2. So the listing would be:

     DIM mcode% 128
     FOR pass%=0 TO 2 STEP2
     OPT pass%

These settings for OPT are useful when all the debugging has been done. An assembly listing would mess up any screen.

Find the faults in the following assembler code.

The 8 faults are marked as follows.

     DIM mcode% 128   ;this does not reserve enough memory
                      ;(add up the message lengths)
     FOR pass%=2 TO 3 ;the loop is allowing errors to be detected
                      ;in BOTH passes.
                      ;This should be FOR pass%=0 TO 3 STEP 3
     OPT P%           ;this is the wrong variable should be pass%
     ADR R0,message%
     SWI OS_Write0
     SWI OS_NewLine
     EQUW This routine has several errors
                      ;this is a string so should use EQUS
     EQUS  some quite simple but others might not be so
     EQUS  obvious to the beginner
                      ;Two extra lines required here
     EQUB 0           ;this is the zero byte marker for SWI "OS_Write0"
     ALIGN            ;required since we are not on a word boundary
                      ;at this point (count up message chrs,
                      ;is the answer divisible by four?)
     ADR R0,message2%
     SWI OS_Write0
     SWI OS_NewLine
                      ;Extra line required here
     MOV PC,R14       ;this is to return to BASIC
     EQUS Have you found all 8 errors?
     EQUB 0

The eighth fault is between the first SWI "OS_NewLine" and .message%. We must branch to the ADR R0,message2% line OR better still place ALL the data together in one block after the MOV PC,R14.

Write a routine in ARM code that will print out the message Enter your full name , then allow the user to type their name, checking for the beginning of each name for an uppercase letter and corrects the letter if the need, displaying the correct version.

The following will do the job but does not allow the use of the delete key (more on input in a later article)

     MODE 28
     DIM mcode% 1024
     FOR pass%=0 TO 3 STEP3
     OPT pass%
     ADR R0,message%     ; load R0 with memory position message% to
     SWI OS_Write0     ; to print out
     MOV R5,#1           ; a flag used for case changing 0=change upper
                         ; to lower, 1= change lower to upper
     SWI OS_ReadC      ; get chr from keyboard
     CMP R0,#13          ;is it return if so then
     MOVEQ PC,R14        ; get out of routine (back to BASIC)
     CMP R0,#32          ; is it space
     CMPNE R0,#ASC(.)  ; or is it a period (full stop)
     BEQ space%          ;if so branch to space%
     CMP R0,#ASC(A)    ;is it a legal chr i.e. A to Z
     BLT inputlp%        ;if below A then get another chr
     CMP R0,#ASC(Z)
     BLE uppercase%      ;if  A up to Z then branch to uppercase% 
     CMP R0,#ASC(a)    ;is it a legal lowercase letter
     BLT inputlp%
     CMP R0,#ASC(z)
     BLE lowercase%      ; if a to z then branch to lowercase%
     B inputlp%          ; if not a legal chr just get another chr
     MOV R5,#1           ; reset flag for changing case
     SWI OS_WriteC     ; write the chr to the screen
     B inputlp%          ;get another chr
     CMP R5,#0           ;check if case changing required
     ADDEQ R0,R0,#32     ;if zero this should be lowercase so add 32 to ASC
     SWI OS_WriteC     ;write chr to screen
     MOV R5,#0           ;reset flag for next chr
     B inputlp%          ;get next chr
     CMP R5,#0           ;check if case changing required
     SUBNE R0,R0,#32     ;if not zero this should be uppercase so subtract
     SWI OS_WriteC     ;write chr to screen
     MOV R5,#0           ;reset flag
     B inputlp%          ; get another chr
     EQUS Enter your full name 
     EQUB 0
     CALL mcode%

Note the use of ; in the listing this stands for REM in assembly. This problem shows the use of a user defined flag. This is a useful technique to check for conditions in ARM programs.

More SWI's

There are hundreds if not thousands of SWI's the programmer can use, some more useful than others so lets have a look at some of them.

This SWI is a hang over from the BBC computer but still has some uses. There are 256 different routines. The number of the OS_Byte routine required is placed in R0, then depending on the routine R1 and R2 have to be setup. E.g. OS_Byte 0 This just prints out the version of RiscOS in your computer and is equivalent to *FX 0.

     MOV R0,#0
     SWI "OS_Byte"

This is equivalent to BASIC's OSCLI command and is useful to execute any legal star (*) command. R0 points to the star command (and data) string, terminating in a null byte. E.g. Output the System$Dir and System$Path variables.

     ADR R0,command%
     SWI "OS_CLI"
     MOV PC,R14
     EQUS"Show System*"
     EQUB 0

If you wish to find the contents of a system variable (e.g. a filepath etc.) then make R0 point to the system variable name and R1 to point to a buffer to receive the data. R2 = maximum length of the buffer. R3 = 0. R4 = 3. This is a powerful call for more details see the PRM's etc.

     ADR R0,varname%
     ADR R1,buffer%
     MOV R2,#12
     MOV R3,#0
     MOV R4,#4
     SWI "OS_ReadVarVal"
     MOV PC,R14
     EQUB 0
     EQUD 0
     EQUD 0
     EQUD 0

In the examples the MOV PC,R14 command is included if you wish to tryout the routine.

Coping with SWI Errors.

Even though we like to think we are capable of producing perfect programs there is always the need to allow for unforeseen circumstances.

What happens if we do not allow enough space for the data to fit in the buffer in the last example?

We do not get an assembler error since the routine is correct within itself. Instead an error will occur when the routine is run. This type of error is known as a run time error. What makes the situation even worse is we are not in a friendly environment, like BASIC, when the routine is running, so any error message will not be helpful and the computer could easily crash. Clearly we need a method of coding which will not upset the running of the ARM code but will still give the programmer a method of detecting and therefore allow for an error at run time. Luckily the designers of RISC OS have thought of this and provide a method of calling SWI's to allow for errors.

Using an X in the name

We can call SWI's, by name, as previously but with an X before the name thus:

     SWI"XOS_CLI" etc.

When this is done RiscOS will not try and generate a run time error but will set the overflow flag to tell the programmer an error has been generated during the execution of the SWI. We can test to see if this flag has been set and take appropriate action.

     ADR R0,command%
     BVC exit%
     ADR R0,errormessage%
     MOV PC,R14
     EQUB 0
     EQUS"There is no such command"
     EQUB 0

It is always best to use the X version of the name. I will deal with run time errors in more detail in a later article.

Storing and Loading data in Memory

At this point I would like to return to the use of memory within ARM code routines.

Loading data from memory.
Using the MOV command and an immediate operand the maximum value that could be loaded into a register was 4096. So how could we get a value greater than this into a register?

There is a LoaD Register (LDR) command which can be used to load data from memory locations.

Load data from a single Location
Syntax: LDR reg,memloc%
E.g. LDR R0,data% where data% is a memory address label.
This is known as PC relative memory addressing.

There is a limit of 4096 bytes the memory address can be from the instruction. This is due to the instruction and memory address data having to fit into the 32 bit ARM command word. To overcome this restriction LDR command can be used in different ways. Each way is called an addressing mode.

Indirect Addressing
This uses a register to specify the memory address. This addressing register can therefore contain any position in memory. When the ARM code program is run the value in the addressing register is looked up, which means the program can define which memory address it requires since the memory address is not fixed in the instruction.

E.g. If R2 was being used as the addressing register and contained the number 1024 then the data at memory address 1024 would be loaded. (See below).

The ARM instruction that would load R0 with the data would be LDR R0,[R2]. ARM machine code supports enhanced indirect addressing.

Pre-indexed Addressing
In this mode an offset is added to the above giving a general syntax:

     LDR destination_reg, [ basereg , offset ]

The offset can be an immediate, register or shifted (see later article) operand thus:

     LDR R0,[R2,R1]   or  LDR R0,[R2,#12]

Here the data would be loaded from memory address equal to R2+R1 (or R2+12) i.e. basereg+offset. The offset, when an immediate operand, must be in the range -4096 to 4096. Using the same example the data this time would be loaded from address 1036 thus:

Write Back
Placing a '!' suffix, makes the instruction add the offset to the value in the basereg after loading the data. E.g.

LDR R0,[R1,#4]!

loads R0 with data from memory address R1+4 then R1 becomes R1+4

This is useful if a data table is set up in memory as after each loading R1 will point to the next memory address.

Post-indexed Addressing
This is similar to pre-indexed addressing but the instruction has built in write back.

LDR R0,[R1],R2

would load data from memory address in R1 then R1 is updated to R1+R2

Using the same memory map as previously:

     LDR R0,[R2],R1

would read data from address 1024 then R2 would become 1036.

To load a table from memory use

LDR R0,[R1],#4

So far we have read a word of data (32 bits). If only byte values are required then a B suffix can be added to the instructions thus:

     LDRB R0,[R1,R2]
     LDRB R0,[R1],R2

Note the B suffix is added after any conditional suffices thus:

     LDRNEB R0,[R1,R2]

This instruction would only be executed if the zero flag is set.

All the different address modes work when storing data to memory using the STR instruction.

     STR R0,memaddr%    :REM store contents of R0 to memaddr% (one word of memory)
     STR R0,[R2,R1]     :REM store contents of R0 at address R2+R1
     STR R0,[R2,#4]     :REM store contents of R0 at address R2+4
     STRB R0,[R2,R1]    :REM store byte value of R0 at address R2+R1
     STR R0,[R2,#4]!    :REM store contents of R0 at address R2+4, R2 becomes R2+4
     STRNEB R0,[R1],R2  :REM store contents of R0 at address R1, R1 becomes R1+R2

All the above would store the contents of R0 in the memory address specified by basereg and offset using the same rules for pre or post indexing.

Setting up a block of memory using a macro assembler routine

Sometimes setting up a block of memory with a table of values during assembly can speed up the ARM code program. The following example shows what is involved.

Plotting Sine waves.
These could take quite a bit of processing if the values had to be calculated during the plotting. Luckily, using BASIC within the assembler, a block of memory can be setup. This block is then used repeatedly for each plot.

To setup a block, construct a function which will work out the required values.

     DEF FNsine(a%,p%)
       EQUD INT(240*SIN(RAD(a%)))

Since we are only dealing with integers at this stage the value returned is 240 times the sine of the angle. Inside the assembler the following can be constructed.

     FOR angle%=0 TO 359
     OPT FNsine(angle%,pass%)
     OPT pass%

The function used in this way is called a macro and the whole process macro assembling.

During assembling we leave the assembler after the label sinetable%. We can now use a FOR NEXT loop to repeat a section. In this example the loop repeats 360 times, hence 360 words (360*4=1440 bytes) of memory will be used. We then enter the assembler in each repeat of the loop. We can use a BASIC function command inside the assembler. The assembler will jump to the start of the definition for the function. In the function we enter the assembler use the EQUD directive to reserve a word of memory and store the required value in the word using the BASIC functions as shown. We then leave the assembler and then leave the function, which returns the assembler back to the the loop. The value our FNsine returns is the pass value for the assembler. Here we leave the assembler to execute the NEXT command. The whole process then repeats, hence building up the table.

Using this method the BASIC program is more compact. The complete BASIC program called SineWaves which produces a set of sine waves is included on the disc. This program produces a sine wave that moves down the screen. A total of 32*360=11520 plots are executed!

Well thats it for this time. Next time I will take a look at the use of stacks and sub routines.

Some problems

  • Write an ARM routine using a lookup table to produce the answer to the (cube-square) of any integer from 1 to 256.
  • Write a routine to input a set of 10 positive integers storing them in memory, and then printing them out. You can use BASIC for inputting, printing and ARM code for storing in memory.
  • Write a routine which will allow a user to enter a password. Each key press should produce a * on the screen. The password should then be checked against one already in memory and if correct a beep should be heard. The password should be case insensitive and at least 10 characters long. (Hint there are a set of SWI's which output single characters: SWI256+42 will output a "*" and a beep is SWI256+7).

Brain Pickard