ARM code for beginners

Part 6 — writing a module

    Last month, we wrote a module of sorts: a piece of code which isn`t a simple stand-alone executable, it needs something else to work with, in this case the screen saver Out To Lunch.  This month, I`ve written a RISC OS module called KeyClick, which makes every keypress generate an audible 'tick' through the speaker.

    RISC OS modules are chunks of code with the filetype &FFA set out in a certain format (documented in figure 1); the parts in grey are not discussed here.  The header contains offsets (from the start of the code) to various pieces of information about the module; the title and help strings are displayed in the module list, and on receipt of a *Help command respectively.  The initialisation code is called when the module is RMLoaded, and the finalisation code when the module is RMKilled.  The start code is provided so that the module can be started as a WIMP task; it is not used here and so set to zero.  The service call handler is a piece of code which receives numbered service calls from the system; these can indicate system events such as the desktop shutting down or the screen mode changing, which a module can act on.  Our KeyClick module doesn‘t need to act on service calls, so this offset is set to zero as well.  The command table points to a list of star commands which the module provides; KeyClick provides just the one rather trivial example to alter the volume of the click.

    The other thing we can assume about writing modules is that when calling our entry points, R13 will always point to the system stack, which is a full, descending one.  Hence we can preserve and restore registers with STMFD / LDMFD as discussed last month.  The initialisation and finalisation code are allowed to corrupt R0-R6, R12 and R14 so we need not preserve these.  In fact R0 is used to return errors (see under 'being eXtra careful').

Keypresses and vectors

    Many events and functions in RISC OS can be intercepted, changed or just noticed by modules.  Certain SWIs, like OS_WriteC, are called through vectors.  This means that RISC OS gives applications a chance to claim these vectors and call their own code either just before, or instead of the code that is normally called.  For this module, the vector we are interested in is known as InsV (number &14 in a series of around &22 vectors), which is called whenever a byte is inserted into a RISC OS buffer.  To claim a vector, we use a SWI OS_Claim, which takes as its parameters the vector number to claim (R0), and the address to jump to when the vector is being called (R1).  R2 should be left as zero for the moment.  OS_Release needs to be called before the module exits (i.e. the finalise code), with the same parameters, to avoid nasty crashes.

    So, once the module has started, it has claimed InsV to point to an address inside its code, labelled .insv_claim in the source.  The only thing we need to do is determine which buffer is being inserted into; the buffer number is passed in R1 and the keyboard buffer is 0, though there is a flag passed in bit 31, so we cannot just use CMP R1,#0 instead, there is the TST instruction.  This performs a 'logical AND' between two registers, but instead of storing the result anywhere, it reflects it in the status flags.  Hence we can check the first 8 bits with TST R1,#255, which will set the Z flag if it is zero, which can be tested for with the EQ/NE condition codes.

The innards of the ARM

    The next difficult bit involves changing processor mode.  This requires some further explanation of the ARM processor; it does in fact have four modes of operation, known as User (USR), Supervisor (SVC), Interrupt (IRQ) and Fast Interrupt (FIRQ).  User mode is what we have executed programs in when we CALL them from BASIC; the most usual mode.  SVC mode is entered when RISC OS is executing a SWI, or engaged in some other activity, and it has separate copies of R13-R14, so that entering SVC mode does not corrupt our usual copies of these registers.  The IRQ and FIRQ modes are entered when an external device needs immediate attention; there are two corresponding pins on the ARM chip which, when set, cause it to break off its normal course of executing instructions, and branch immediately to the appropriate IRQ or FIRQ handler.  The program counter is used to indicate the processor mode (2 bits), the state of the interrupt system (2 bits to disable IRQS and/or FIRQs), to store the status flags (4 bits) and keep the ARM`s program counter (the remaining 24 bits) all in one word, as shown in figure 2.  The point of having these different registers is that interrupt code can be written to execute quickly, without needing to bother preserving critical registers.

    Now, when RISC OS enters our initialisation or finalisation code, it enters in SVC mode.  This means we are now using the separate copy of R14 (called R14_SVC for clarity).  So if we execute a SWI before preserving R14_SVC, the new SWI will preserve the return address in R14_SVC and corrupt the return address from the initialisation code.  Hence we must preserve R14_SVC on the stack with a STMFD R13!,{r14} instruction before executing any SWI from within SVC mode; this is normally taken for granted, but is worth mentioning for the next point.  When we claim a vector with OS_Claim, the vector code (in the case of InsV, at any rate) can be entered in IRQ mode.  This means that before the interrupt occurred, we could have been in SVC mode, and there is a chance that R14_SVC is being used for something.  Hence if we call a SWI from IRQ mode, we risk corrupting R14_SVC and causing crashes.  However, we cannot simply push R14_SVC onto the stack, since in IRQ mode, we only have access to R14_IRQ.  The solution is to change to SVC mode, then preserve and restore R14, executing our SWIs inbetween, and change back to IRQ mode.  So:

    We can restore our old processor mode with TEQP R9,#0; note that after changing processor mode, the processor will always skip the next instruction, so it needs to be a NO-OP; Acorn recommend using MOV R0,R0.

    So, to finish our explanation, once we have established that a byte is being inserted into the keyboard buffer, and we have changed processor modes, we can execute the Sound_Control SWI, change the processor mode back, and return from the vector routine.

Being eXtra careful

    You`ll notice some SWIs have an odd X prefix on them this month; this indicates a different way of indicating errors.  Normally, if a SWI causes an error, it kills the current program off and the OS error handler is called.  For modules, this is not appropriate; we need to handle errors ourselves, and pass them on neatly.  This is why we have the 'X' form of every SWI; instead of calling the error handler, if there is a problem the SWI returns an error block pointer in R0, and sets the oVerflow flag, which can check for with the VS/VC condition codes.  The initialisation code, finalisation code the code for the *KeyClick command all expect the module to do this if there‘s a problem in any area, so that the OS can display the error without breaking any other programs, which is what would happens if the normal error handler was called.  Hence, if we do have an error, we must exit straight out of our routine again, preserving the error block pointer in R0 and the V flag.

    Keyclick uses OS_ReadUnsigned to convert a string to a number (the VAL function in BASIC), but it generates an error with 'bad' strings containing letters or symbols, which the user could quite conceivably enter by accident.  Hence the X form is used and the routine exited if an error occurs, but for functions like OS_NewLine, we don‘t need to bother checking for errors since they won`t happen.

    Do go through the source with the StrongHelp manual, looking up SWIs if you need to, and with this article to explain the hard parts.  Next month, I`ll expand on modules some more.