by Martin Penny
Now the basic instruction set has been described, we can now move on to some programming examples. To start off with, I'll go over accessing BASIC variables; once that's done, I'll then move on to describing how BASIC loop and control statements can be simulated.
The reason I want to cover the subject of BASIC variables comes back to a point I made back in the first part of this series - that one major reason assembly language is used in programs is speed. Not all of the code in a BASIC program needs to be translated into assembly language, as the translation may not be simple; on top of this, the replacement code will not always be noticeably faster than the replaced code.
So, in a BASIC-assembly language program, there will be a need for the assembly language part of the program to access data from the BASIC side, and vice versa. In most of the example programs used in previous parts of this series, I've put the data directly into the assembly language code. Altering the data means either reassembling the code each and every time any is changed, or having to poke around in memory. This approach to assembly language is, at the very least, inefficient; at worst, it more than negates any potential speed gain. Hence, assembly language should be converted to machine code as few times as possible, and greater use needs to be made of parameters and variables.
As previously mentioned, BASIC copies "A%" to "H%" into registers "R0" to "R7"; this makes passing simple information to machine code programs fairly easy. This information may be anything that can fit into 32-bit variables, including numbers and pointers. Above and beyond this, "CALL" may be used to pass a list of variables; "R9" points to the list of variables, and "R10" holds the number of entries. To make things more complicated, "R9" points to the end of the list, not the start; even so, accessing BASIC variables is still fairly easy. The BBC BASIC Guide contains a fairly detailed description of how parameters are passed via "CALL", but to demonstrate the point, figure 1 is a program that will print a string variable. Enter the program and run it; when prompted, type in some text and see it printed.
Now for a description of how the program works. Much of the BASIC side of the program is the same as in the example programs from previous parts of this series, so should be familiar. The major difference is in the "CALL" at line 960, where the variable "A$" has been added to the end of the line; this causes "R9" and "R10" to be set appropriately. There are, however, more points of interest within the assembly language side of the program; first, though, to parts that are the same as in previous examples. A number of registers are preserved at the beginning, and restored at the end; the "Print%" code is also familiar and "standard".
The program starts at line 230 by checking the number of parameters. If there are too few, or too many, "R0" is set up, and the program branches to the "Print%" code. The second step is, at line 280, to check the type of parameter; if not a string, "R0" is set up, and control again passes to the "Print%" code. Next, the program reads the pointer to the string - line 310 - and its length - line 320 - which is checked; if zero, "R0" is set up, and control yet again passes to the "Print%" code. In each case, the code is simplified by the use of conditional instructions; you can try stepping through the code through, with alternative variations to satisfy yourself that it works as expected.
The BBC BASIC Guide doesn't specify anything about the end-of-string terminator, so, in order for the string to be suitable for the "OS_Write0" routine, a zero-terminated copy needs to be made. Lines 360 to 420 fetch the pointer to the string proper. As this is not necessarily word-aligned, a simple "LDR" will not suffice, as its effectiveness can't be guaranteed; instead, the four bytes of the pointer have to be fetched one at a time, and reassembled into a word on the fly. Finally, line 430 sets up the destination pointer for the string, while lines 440 to 480 copy the string, relying on the "LDR" instructions to update the pointers. Finally, lines 490 to 510 insert a zero byte at the end of the string before it is printed via the "Print%" code.
Okay, now for a bit of a challenge. Taking figure 1 as it stands, how would you change it to print the string backwards? One way of doing it is in figure 2 - but try not to peek until you've had a try!
In figure 2, the preamble to the main loop is the same as in figure 1; the changes start just before the loop, when the end-of-string zero byte is put into place. The loop counter is updated at the start of the loop, in order to get the string offset correct; as neither the "LDRB" nor the "STRB" alter the flags, the end-of-loop branch is still valid. The use of the loop counter in the "STRB" is also perfectly valid, and a common occurrence, whatever the programming language. As with the code in figure 1, try following the code before running it, just to satisfy yourself it will work as expected, then give it whirl!
There is one other type of "simple" string variable, that described as "$factor" - in other words, a quoted literal string. As a consequence, this type of string is quicker to read than a string variable proper. Making use of integer variables is easier than string variables, as getting hold of the integer variable's value is essentially the same as reading the string variable pointer in these two example programs - lines 360 to 420. As BASIC uses 32-bit integers, this matches up nicely with ARM register sizes. Byte values are similarly easy to use.
Before I go on to cover floating-point variables, a little bit of a diversion is in order. "R14" is normally used to hold just a return address, ready for a "MOV R15,R14" instruction. In BASIC, it's not quite that simple - "R14" is a pointer to a word-aligned table of word-sized values. The first of these is a branch instruction, so a "MOV R15,R14" is redirected back to BASIC properly. The remaining values are pointers to useful routines within BASIC, and allow for close integration between BASIC, and user-written machine code. I won't go into a huge amount of detail in this article, but all the routines are documented in the BBC BASIC Guide.
Floating-point variables can be awkward to use, and this is down to the format used by BASIC. BBC BASIC I, as used on the original BBC Micro, used a five-byte floating-point format; this was inherited in later versions, up to and including the ROM-based BBC BASIC V on RISC OS computers. A slightly modified version, BBC BASIC VI, has an eight-byte floating-point format, compatible with the "double precision" format used by the ARM's floating-point instructions.
Floating-point variables supplied via a "CALL" may be read - and converted to a temporary expanded format - without great difficulty by using routines in the table pointed to by "R14". The table also allows simple calculations to be done, as long as they are based on the four operators - "+-*/" - and "square root". Finally, the results of these calculations may be written back to floating-point variables, again via the table.
On the downside, there are several main drawbacks with floating-point variables, and they are as follows. Firstly, the "standard" five-byte format is not compatible with the floating-point instructions, nor is the temporary, expanded, format. Secondly, BASIC doesn't offer many floating-point maths routines accessible from assembly language routines - anyone wanting to write more than a four-function calculator will be disappointed. Add to this the fourth point, that BBC BASIC VI has a native format compatible with the floating-point instructions, but still - as far as I know - little or no support for anything other than the basic ARM2 integer instructions - this means there's no easy way of by-passing BASIC's routines.
These four points together make it difficult to convert maths routines to assembly language, an area that could benefit a lot from the floating-point hardware in ARM/FPA combinations, including the ARM7500FE processor. Because of this, many programmers have, in the past, written custom routines that operate upon fixed-point values in the ARM's integer registers, in a style similar to the Draw scaling factors.
One last comment before I move on, and that is to do with arrays. Arrays may be passed by "CALL" as easily as any other type of supported variable, and appear as indexed lists of the component variable type - string, integer, or floating-point.
As I've now - briefly - covered accessing BASIC variables, I can now move on to control statements - code equivalent to "IF...THEN" and friends. In fact, I've already used a couple in the example programs included in these articles. For instance, have a look at figure 1; it contains sections of code that are equivalent to "IF...THEN", and others equivalent to "FOR...NEXT".
To start off with, each piece of code where a register is tested and followed by one or more conditional instructions, the last of which is a branch, is effectively an "IF...THEN" block. "IF...THEN...ELSE" can be simulated through use of mutually exclusive condition codes - the test can be followed by a sequence of conditional instructions, some of which execute on the test being "true", and the others on it being "false". As long as you don't go overboard with this, it can be quite effective in eliminating branches. Moving on to "FOR...NEXT", the code headed by the "Loop%" label in each of the programs can be seen to be a loop, with "R1" being the control variable; the loop counts down from the length of the string to "1" ("0" is the end-of-loop limit test).
However, the loops used in figure 1 is not just the equivalent of "FOR...NEXT", but as "REPEAT...UNTIL" too. As with the "FOR...NEXT" loop, the "REPEAT...UNTIL" loop is controlled via "R1"; as long as it remains positive, the "UNTIL" test remains false, and hence the loop carries on. The "WHILE...ENDWHILE" block is slightly different to previous examples; figure 3 is a revised program that is its equivalent. Note that the test-and-branch sequence is now completely at the beginning of the loop, with an extra branch at the end; this is required to prevent the loop from unravelling.
The various loop structures have tended to simplify down to variations-on-a-theme, depending on exactly where in the loop the end-of-loop test is performed. Indeed, I could go as far as saying that the loop structures are not any different to dressed-up combinations of "IF...THEN" and "GOTO" - that's certainly how the assembly language equivalents have to be coded.
On a similar note, the other main control structures tend to boil down to similar assembly language routines. Figure 4 gives an example of such a program; it print a message depending on which key you press. Lines 220 to 250 "weed out" the ASCII control codes; this code is especially interesting as it is given as an example in a range of documents - including ARM Ltd.'s own reference material. It may seem a little strange to have two different test instructions back-to-back with no code separating them, but this compound test relies on the differences between "CMPS" and "TEQS". The first test instruction alters all four main flags, while the second alters only "N" and "Z" (it does not change "C", as "Op2" is not shifted). This difference allows the "LS" condition code to do the required job nicely and neatly.
As the particular ASCII codes I'm looking for are a short, sequential range, the following code is simpler than it otherwise could have been. The "SUB" instruction at line 260 changes the contents of "R0" to something more convenient; as "negative" values are just the same as large "positive" values, the unsigned "HI" condition filters out any remaining unwanted ASCII codes. That leaves a value in the range "0" to "9" in register "R0" which can be used as an index into the table of addresses pointed to "R2"; as addresses are word-length, the "LSL #2" multiplies the contents of "R0" by four before adding it to "R2". In this program, the addresses are those of the strings to be printed, but they could equally easily be addresses of program code; by changing the "LDR" at line 310 to use "R15" as its destination register, or following it up with a "MOV R15,R0" would simulate a branch instruction.
Figure 4 is the equivalent to BASIC's "ON..." structures, or a simple calculated "GOTO"; this can be done as the relevant data I'm looking for - as narrow range of ASCII codes - is itself simple. If the data were more complex or more arbitrary, a different solution would need to be used - the equivalent to BASIC's "CASE...OF" structure - and one such solution is given in figure 5.
This example starts off in a similar fashion to that in figure 4, only differing in how it works out which string to print. The main loop is at lines 270 to 330, with "R2" already having been set up to point to the main data table at line 260. The loop relies on the fact that the table is a list of pairs of words - one word containing a value, its partner an address - and terminated by a pair of zero words. A value is loaded from the table, and "R2" is updated automatically to point at the first of the next pair of words. If this value is zero, the loop has reached the end of the table and no match has been found, so the loop terminated with a suitable message.
Next stop is to check is the entry from the table is the same as the one we're looking for - if not, we go round the loop one more time. If we have a match, we need to get the corresponding address; as "R2" is not pointing at quite the right position in the table, an offset of "#-4" is used as a "correction". One this has been done, the string can be printed. As with the example in figure 4, the code in figure 5 could easily be adapted to addresses of program code, not just address of strings.
The aims of these routines could be achieved trough a long sequence of individual "IF...THEN"-style tests, but this can become unwieldy extremely quickly, not to mention the fact that "simple" compare instructions are limited in two major ways. Firstly, ARM arithmetic-type instructions cannot directly handle the entire range of 32-bit values, so both the value being tested and the value it is tested against would still have to be loaded from memory. Secondly, handling data other than 32-bit words - usually strings - the data has to be held in memory, rather than in registers.
That basically wraps it up for this part of the series; in the next part, I'll cover both subroutines, and some of the potentially more useful software interrupt routines.
Return to ARM Code Tutorial 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.)