ARM code for Beginners
Part 3: Two pass assembling and SWI routines
Brain Pickard explains how anyone can program in ARM code.
Before we start this issue here are the solutions for the problems posed in Part 2.
Construct a BASIC program which includes an ARM code routine to place user values in registers R3 and R4 that will test the values in each register and give a zero if R3 = R4, one if R3 < R4 and two if R3> R4. Answers to be given in R0 for the BASIC program to print out.
This is an exercise in using CMP and conditional suffixes.
DIM mcode% 1024 P%=mcode% [ CMP R3,R4 MOVEQ R0,#0 MOVLT R0,#1 MOVGT R0,#2 MOV PC,R14 ] INPUT"Enter an integer value "C% INPUT"Enter another integer value "D% A%=USR(mcode%) PRINT"Returned value is ";A% END
Find the errors in the following....
The errors were as follows:
MOV R2,#-6 cannot use a negative sign in an immediate constant. Use MVN R2,#6 instead (MVN is move negative into Register) MOV R1,#257 an illegal value for an immediate constant. You could get round this problem by MOV R1,#256 followed by ADD R1,R1,#1 lp% missing . for a label definition.
Construct a BASIC program using an ARM routine which will check if the value in R1 is a possible ASCII code for a capital letter. If it is then it will place in R0 the corresponding lower case ASCII code, which the BASIC program will print out.
Upper case ASCII codes start at 65 (A) and end at 90 (Z). Lower case ASCII codes start at 97 (a) and end at 122 (z). You can see this by opening a blank edit window holding down the ALT key on the keyboard and typing 122 on the numeric keypad while keeping ALT held down. When you release ALT the letter "z" will appear since 122 is the ASCII code for "z". The difference between uppercase and lowercase ASCII codes is 97-65=32, so to change from uppercase to lowercase we simply add 32.
DIM mcode% 1024 P%=mcode% [ CMP R1,#64 ADDGT R0,R1,#32 CMP R1,#90 MOVGT R0,#0 MOV PC,R14 ] INPUT"Enter a single letter "A$ B%=ASC(A$) A%=USR(mcode%) IF A%=0 THEN PRINT"This was not an uppercase letter" ELSE PRINT"The lowercase equivalent is "CHR$(A%) ENDIF END
Using a loop, design an ARM routine which mimics integer division by repeated subtraction. The routine should give the correct answer back for the BASIC program to print out.
I will place the number to be divided in R1 and the divisor in R2
DIM mcode% 1024 P%=mcode% [ MOV R0,#0 .divlp% CMP R1,R2 ;note position of comparison to allow for a zero answer. ADDGE R0,R0,#1 ;all the following instructions executed if R1>=R2 SUBGE R1,R1,R2 BGE divlp% MOV PC,R14 ] INPUT"Enter the number to be divided "B% INPUT"Enter the divisor "C% A%=USR(mcode%) PRINT"The integer division answer is ";A% END
You might have found other equally correct solutions. This often happens with programming, for example problem three could have been coded thus:
[ MOV R0,#0 CMP R1,#64 BLE exit% CMP R1,#90 BGT exit% ADD R0,R1,#32 .exit% MOV PC,R14 ]
If you use this method and try to assemble the code an error occurs. This is due to the branch commands requiring the value of exit% BEFORE exit% has been defined. This is called a forward reference. To overcome this problem we have to assemble the code twice, the first time with error checking switched off (to make sure any forward referencing does not stop the assembler) and the second time with error checking switched on. This is called two-pass assembling. This works because on the first pass the assembler finds the position of exit% so that when the 2nd pass occurs the forward reference to exit% does not cause an error as the assembler knows where exit% is located.
Directives
So far we have only used assembler code inside the assembler. We can also use directives. These are commands directed to the assembler but do not appear the final code. One such directive is OPT (not to be confused with *OPT in RISC OS). OPT switches options on and off depending on a value given.
For example
OPT 3 error checking active and assembly listing active. This value is the default value.
The OPT values are shown below are all derived using a 4 bit nibble ROEL
- R is the Range bit. (bit3)
- O is the Offset bit. (bit2)
- E Error checking bit. (bit 1)
- L Listing generation bit. (bit 0)
For Error checking and listing then %0011=3 i.e. we are turning on bits 0 and 1. The value zero has all the effects switched off. Range checking and offset assembly will be discussed in later articles for now we will use values which switch these options off.
Returning to the problem we construct a two pass assembly thus:
DIM mcode% 1024 FOR pass%=0 TO 3 STEP3 P%=mcode% [ OPT pass% MOV R0,#0 CMP R1,#64 BLE exit% CMP R1,#90 BGT exit% ADD R0,R1,#32 .exit% MOV PC,R14 ] NEXT
Placing the assembly routine inside a FOR NEXT loop forces the two passes. The value for OPT is set by the FOR NEXT loop counter. During the first pass the errors will still occur, but the memory address at .exit% will be remembered for use in the second pass. During the second pass the code will be assembled correctly. From now on I will assume this structure in any routines. You will notice that on the first pass no listing is produced, this speeds up the assembly and in any case a listing is superfluous when error checking is switched off.
Using RISC OS SWI routines
When designing ARM code on a RISC OS computer most of the SWI routines are available. These are called by using SWI with their name or number. The programmer must set up any register values before calling the SWI and realise that some register values will be changed by the SWI. For a list of SWI‘s refer to PRMs and/or other manuals. In these articles I will just outline the general use of SWI‘s.
Consider the following.
DIM mcode% 1024 FOR pass%=0 TO 3 STEP3 P%=mcode% [ OPT pass% CMP R0,#64 BLT exit% CMP R0,#90 BGT exit% ADD R0,R0,#32 SWI "OS_WriteC" .exit% SWI "OS_NewLine" MOV PC,R14 ] NEXT PRINT"Auto changing of character case" REPEAT PRINT"Press a Uppercase character (A to Z)"; A%=GET CALL mcode% UNTIL A%=13 END
This is an extension of the upper to lower case routine. The SWI "OS_WriteC" only requires an ASCII code of a character in R0. It then outputs the character to the active output (the screen). The SWI "OS_NewLine" does not require any data, it outputs a newline (carriage return) code.
Another useful SWI is OS_Write0. This requires R0 to point to a null terminated string of characters which it will output. We therefore need a method of loading a register with a memory address and storing a string of characters at that address.
The ADR directive
This directive tells the assembler to place a memory address in a register. It is not an ARM code instruction, it can produce more than one instruction. To the user it acts as if it is an ARM instruction. For example
ADR R0,string%
This would place the memory address, given from a known label .string%, into R0.
Storing data within ARM code.
There are 4 directives.
EQUB Reserve one byte of memory EQUW Reserve two bytes (a word 16 bits) EQUD Reserve 4 bytes (a double word 32 bits) EQUS Reserve a string of characters (up to 255)
We would expect a word in ARM to be 32 bits, which it is, the above names are a throwback to the BBC computer days which had a word of 16 bits.
You can use alternative names for the following: For EQUB you can use DCB
EQUW DCW EQUD DCD
But I tend to use the EQU versions as I find these more consistent. For example to place a string of characters at a memory address labelled string% ending with a zero (null) byte.
REM To print out a string of characters using SWI "OS_Write0" DIM mcode% 1024 FOR pass%=0 TO 3 STEP3 P%=mcode% [ OPT pass% :REM OPT for two pass assembling ADR R0,string% :REM store memory address at string% in R0 SWI "OS_Write0" :REM call the SWI SWI "OS_NewLine" :REM call the SWI MOV PC,R14 :REM return to BASIC program (END statement) .string% :REM label for memory address EQUS "A string of printable characters" :REM place these chrs in memory EQUB 0 :REM ending with a null byte ] NEXT CALL mcode% END
We have to be careful when using EQUB, EQUW and EQUS since they result in the assembler moving on any number of bytes. ARM code instructions have to start on word boundaries i.e. an offset from the start that is divisible by 4. If we wish to repeat the process we might expect the following to work.
REM To print out a string of characters using SWI "OS_Write0" FOR pass%=0 TO 3 STEP3 P%=mcode% [ OPT pass% ADR R0,string% SWI "OS_Write0" SWI "OS_Newline" B nxtstring% .string% EQUS "A string of printable characters" EQUB 0 .nxtstring% ADR R0,string2% SWI "OS_Write0" SWI "OS_NewLine" MOV PC,R14 .string2% EQUS "Another string of characters" EQUB 0 ] NEXT CALL mcode% END
An error may well occur due to ADR R0,string2% starting at 45 bytes from the start. It should start at 48 bytes (nearest word boundary - using integers you can divide 4 into 48 but not into 45). To force this to happen we use the ALIGN directive thus.
DIM mcode% 1024 FOR pass%=0 TO 3 STEP3 P%=mcode% [ OPT pass% ADR R0,string% SWI "OS_Write0" SWI "OS_Newline" B nxtstring% .string% EQUS "A string of printable characters" EQUB 0 ALIGN .nxtstring% ADR R0,string2% SWI "OS_Write0" SWI "OS_Newline" MOV PC,R14 .string2% EQUS "Another string of characters" EQUB 0 ] NEXT CALL mcode% END
Other useful SWI‘s
SWI "OS_ReadC"This SWI waits for a key press and places the ASCII code of the key in R0
SWI "OS_ReadLine"This requires R0 to point to a buffer, R1 equals buffer length, R2 equals minimum ASCII code of character and R3 equals the maximum ASCII code of character to be placed in buffer.
SWI "OS_ReadUnsigned"This changes a string of numbers into a real unsigned number. R0 = the number base, R1 points to the memory containing the string of numbers. On exit R2 will contain the real number.
That's it for this time more on SWI‘s and how to store and load from memory in the next article.
Problems to solve
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?
Find the faults in the following assembler code
DIM mcode% 128 FOR pass%=2 TO 3 P%=mcode% [ OPT P% ADR R0,message% SWI "OS_Write0" SWI "OS_NewLine" .message% EQUW "This routine has several errors" EQUS " some quite simple but others might not be so" EQUS " obvious to the beginner" ADR R0,message2% SWI "OS_Write0" SWI "OS_NewLine" .message2% EQUS "Have you found all 8 errors?" EQUB 0 ] NEXT
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.