Acorn User

ARM CODE TUTORIAL

by Martin Penney

Part 6 - Subroutines And Software Interrupts

In this part of the series, I'll be going over subroutines within ARM assembly language, and mentioning some of the potentially more useful software interrupt routines. I know I've already gone over the relevant instructions while covering the ARM instruction set, but I feel there's a few finer points of using "B" and "BL" that I need to cover. In addition, all I've really mentioned with regards "SWI" is how the "parameter" is divided up, and not a great deal about individual routines. Hence this part of the series.

First off, notes about using "BL". As I mentioned back in an earlier part of this series, whenever "BL" is used to call a subroutine, the address of the instruction following the "BL" is copied into "R14". This means that, at the end of the subroutine, a "MOV R15,R14" instruction can be used to do the same thing as "RTS" on a 6502 processor. Figure 1 gives a simple use of "BL".

-- Figure 1 --

The program starts off by stacking a number of registers, as in previous examples; this includes the initial contents of "R14". This means that the return address to BASIC is safely preserved, leaving "R14" available for use within the program proper. After calling the "OS_NewLine" routine, the program sets up "R0" to point at a string, then uses "BL" to call the "Print%" code. In previous examples, this code was part of the main body of the program, but his time, I've deliberately separated it out into a subroutine. However, it still does the same as before - just printing the string pointed to by "R0".

During the "Print%" code, "R14" points at the "SWI" at line 260, the instruction immediately following the "BL". Hence, when the "MOV R15,R14" at line 330 is executed, control passes to that "SWI" at line 260, picking up execution where it left off. The program finishes off by calling the "OS_NewLine" routine, restoring the registers from the stack and returning to BASIC,

A more complex example is given in figure 2. A lot of the code is the same as in figure 1, though there are a number of additions. To begin with, the main body of the program now calls the "Print%" code twice, once either side of a call to the "LevelOne%" code. The "Print%" code is as in example 1; however, the "LevelOne%" code is more interesting, as it too calls the "Print%" code. If you remember what I've said about "BL", you'll realise that the "LevelOne%" code overwrites "R14" when it calls "Print%"; this has the upshot of erasing the previous contents of "R14", required to return to the main body of the program - line 270, to be exact.

-- Figure 2 --

This is a problem - if "R14" is corrupted and its original contents lost, there's no way of returning control back to the original calling code. This problem is easily avoided by saving the contents of "R14" on the stack, by using the "STMFD" and "LDMFD" instructions as used in the "LevelOne%" code.

A "BL" instruction can be "faked" by loading "R14" with a "return address" and following this by either a "B", or loading "R15" directly with the address of the subroutine. Figure 3 is an example of this in action; line 260 load the address of "Return%" at line 280 in to "R14", and line 270 loads "R15" with the address of "LevelOne%". Compare this to the equivalent code in example 2, at line 260, where just a "BL" is used for the branch. This particular way of doing things is useful for branches or jumps to "distant" addresses in "32-bit" mode - addresses more than 32Mbytes' distance from the faked "BL". It can also be used in code akin to BASIC's "CASE..." structure, as given in the previous part of this series.

-- Figure 3 --

That wraps it up for my coverage of subroutines; now I can move on to software interrupt routines.

When "SWI" instructions are used in assembly language, BASIC needs to translate its name into its corresponding number; similarly, when you use the "*MemoryI" command to disassemble an area of memory, it needs to translate "SWI" numbers into names. Fortunately, there are a couple of ready-made routines that will allow you to do just that, and figure 4 gives a program that quite neatly demonstrates the first of them. To use this program, type it in and run it; when asked, enter a number. A blank entry or a negative number will stop the program, while a positive number will produce a list of "SWI" names; the number is filtered to eliminate non-RISC OS "SWI" routines - see my notes in a previous part of this series, or other documentation, for more information.

-- Figure 4 --

This example should have allowed you to see what "SWI" names are defined on your computer; this list will vary from one computer to the next, depending on the version of RISC OS and any expansion cards fitted, and any utility modules that have been loaded. Some of the "SWI" names that are common to all versions of RISC OS are as follows.

One call I have already used many times is "OS_Write0", which prints a string pointed to by "R0". Related calls include "OS_WriteC" - which prints the character whose ASCII code is in "R0" - and "OS_WriteI+n" - which prints the preset character, ASCII code "n".

In a similar vein, there are a number of routines for reading input from the user, usually via the keyboard. Figure 5 is a previous example program, modified to use "OS_ReadC" to read a single character from the keyboard. Similarly, figure 6 is a different example program, changed to use "OS_ReadLine" to read a whole string from the keyboard in one go.

-- Figure 5 --

-- Figure 6 --

In figure 5, the "OS_ReadC" call is at line 250; this waits until a key is pressed before returning, though if "Escape" has been pressed or an error has occurred, it returns with the carry flag set. If no error has occurred, the program carries on past line 260 an prints a string, according to the key pressed. On the other hand, if "OS_ReadC" has flagged an error, line 260 diverts to program to the "Error%" code. This uses "OS_ReadEscapeState" to check to see if "Escape" has been pressed; if not, this call returns with the carry flag clear, and the error code uses "OS_GenerateError" to generate a general error. If "Escape" has been pressed, the "Acknowledge%" subroutine is called by line 490. The "Escape" state is cleared by using an "OS_Byte" call, before lines 560 and 570 set the carry flag; this ensures that the code from line 500 onwards continues executing correctly. (Note I've used an arithmetic instruction to do this - this makes sure the code works properly on all current ARM processors.) On return from the "Acknowledge%" subroutine, the "Error%" code generates an "Escape" error.

The changes made to form figure 6 work in a similar fashion; the "OS_ReadLine" call is at line 330, and any "Escape" errors are caught by line 340 and passed on to the "Escape%" code. The "Escape" condition is cleared at lines 580 and 590, before lines 600 and 610 generate an "Escape" error. There is no need to separate out any general errors, as - at least in my copies of the PRMs - "OS_ReadLine" only ever flags an "Escape" error, whilst "OS_ReadC" could - apparently - generate a more general error.

On the subject of errors, RISC OS already uses the overflow flag to indicate an error condition; however, this is usually trapped before control returns to the calling program, and is instead passed on to the local error handler, without involving the program itself. (With BASIC programs, the local error handler would be BASIC.) This is the reason why both "OS_ReadC" and "OS_ReadLine" use the carry flag for indicating an "Escape" condition - RISC OS doesn't trap the error. An alternative way to stop this trapping from taking place is to use the "X" version of "SWI" routines - for example, using "XOS_ReadC" instead of "OS_ReadC". This means your programs has to have its own means of reporting errors; however, I'm getting ahead of myself, as I'll be covering this topic in more detail in the next part of the series.

On to the next program, as given in example 7. I've included this particular example, as most people will want some way of displaying the output of their own code. One way is to use BASIC variable to record the results, and then use BASIC code to format and print the data. Another way is to use a buffer of some kind, dump the data into the buffer - in some semi-formatted state - and again use some BASIC code to do the formatting and printing. Neither method is always convenient or appropriate; indeed, you may easily end up with far more code doing the converting and formatting that doing anything "useful". RISC OS has a range of built-in conversion routines, some for "inward-bound" data - for example, "*" command parameters - and some for "outward-bound" data - for example, data to be printed on-screen.

-- Example 7 --

The program starts off by getting the base of the "OS_WriteI+n" calls to the variable "WriteI%"; this is, basically, always 256, but the "SYS" at line 110 is a convenient way of not having to remember exactly what it is. Within the assembly language section of the program, "R8" is initialised as a loop counter, "R9" as a pointer, then - at line 270 - registers "R0" to "R7" are dumped to reserved memory; writeback is not used, in order to preserve the initial value of "R9". Inside the loop, the program uses "OS_WriteI+n" calls for some formatting, "OS_WriteC" to print a letter in the range "A" to "H", then reverts to "OS_WriteI+n" calls for further formatting.

To print the contents of a register, "R0" is loaded with the value to be converted, "R1" with a pointer to a buffer, and "R2" with the buffer's size. There is large group of conversion routines, with names along the lines of "OS_ConvertNameNumber"; all convert the value in "R0" into a zero-terminated string that is placed into the buffer pointed to by "R1". In this example, "OS_ConvertHex8" is used. On exit from the conversion routine, "R0" now points to the buffer - the old value of "R1" - and so "OS_Write0" can be called immediately. A "newline" character is printed, the loop counter is updated, and the loop continues for the remaining registers.

The last "major" RISC OS routine I'll mention here is "OS_File". An example of "real-life" use is given in figure 8, which is taken from my application, "!68Host". As this application starts up, it tries loading two files from disc into RAM; these files are used as ROM images within the emulator code. In each of the cases "OS_File" is used, I have used the "X" version of the call; the primary reason for this is to do with error handling. As I mentioned earlier, I'll go into error trapping and handling in more detail in the next part of the series; however, I shall make one point here. "!68Host" is a RISC OS application, and I don't really want an error generated by "OS_File" to cause the program to quit in an uncontrolled manner. This explains both the use of the "X" option - which returns errors back to the program, rather than allowing RISC OS to handle them - and also the "paranoid" double-checking used within the routine.

-- Figure 8 --

The routine tries loading each file in exactly the same way, so the same sequences of code appear twice over - this is part of the reason for the length of the routine. To start off with, the routine checks for the presence of each file via "OS_File" 23; if the relevant file is either missing, or not a file - in other words, a directory or application - an error is generated via the "RomImages_Error%" routine. The size of each file is also checked via "OS_File" 19, and an error is generated in a similar fashion if either of the files is the wrong size. Finally, once this is done, the routine assumes it is safe to load both files, and does so via "OS_File" 16.

At the end of the main "RomImages%" routine, an arithmetic instruction is used to clear the overflow flag; the routine then returns to the calling code. The "RomImages_Error%" routine saves "R14", calls the "ReportError_OK%" routine - which generates a standard RISC OS error-box - restores "R14", sets the overflow flag, and returns to the calling code. The calling code can then check the state of the overflow flag, to see whether the "RomImages%" routine was successful or not.

I'll wrap up this part of the series by having a quick look at one last "SWI" - "OS_Exit". To this end, have a look at figure 9; I know this particular example has already been used a couple of times within this series, but it is appropriate for the task here.

-- Figure 9 --

So far, I have tended to assume that most of the assembly language code you'll be writing will be for use within BASIC programs. This has made the example programs somewhat simpler - BASIC has already set up a stack, accessible through "R13", and a "MOV R15,R14" is sufficient to return control back to BASIC, once a routine has finished. However, as BASIC allows for "offset assembly", it is easy enough to use BASIC's inline assembler to create stand-alone CLI or Desktop programs. I'll cover writing such programs in a bit more detail in the last part of this series, but suffice to say that these programs are not - generally speaking - called from within BASIC, and so have no "calling program"; nor, for that matter, is "R14" valid when the CLI or Desktop program starts.

That is where "OS_Exit" comes in - all it does is return control back to RISC OS once a CLI or Desktop program has finished; the call does not return to the program at all. Normally, as with this example, "R0", "R1" and "R2" are all set to zero, indicating no problem. Return codes are used to indicate some kind of fault, though not necessarily an error; if you wish to set a return code - the system variable "Sys$ReturnCode" is set to this value - load "R1" with "&58454241" (the string "ABEX"), and "R2" to the required value.

That is all for this part; in the next - and penultimate - part of this series, I'll go over a number of related topics, including debugging and error trapping and handling.

Return to ARM Code Tutorial index

Return to Tutorials index

Return to Main index

This CD and its design is Copyright © 2000 Tau Press Limited. It may not be copied or distributed without the prior consent of Tau Press. Failure to abide by this may result in prosecution. (That doesn't mean the contents are our copyright, just the linking pages that we created and the CD itself.)