Programming for non programmers
Part 5 - Communicating with the Operating System
David Holden continues his series.
So far all the programs I have used as examples have only interacted in a very simplistic way. They have obtained data from the keyboard using the INPUT keyword and then PRINTed output direct to the screen. Obviously programs need to be able to do a lot more than this. They need to be able to 'talk to' the computer's operating system (OS).
In effect this is what INPUT and PRINT do, but there are so many things that a program might need to do that it wouldn't be possible to have a BASIC keyword or function for more than a very small number of them. What is needed is a standard method for programs to communicate with the OS. This is provided by the Software Interrupt or SWI.
In most RISC OS BASIC tutorials it is common to leave SWIs until much later. However, it is essential to understand how to use them if you are going to make your programs work properly in a multi-tasking environment like the Desktop. If your program is going to exist in the Desktop all keyboard and mouse input and all screen output would usually go via the WIMP, and that means using SWIs.
SWI's from BASIC
SWIs have to be able to be used from any computer language and from machine code. This means the system has to be extremely flexible. To understand how it works you will need to know a little (but only a very little) about how the computer's ARM processor operates.
The processor has 16 registers, and as these are all 32 bit registers they can each hold and manipulate 4 bytes or one 'word' of data. Three of the registers, R13, R14 and R15, have special functions, but the remaining 13 are 'general purpose'. To use a SWI any data or parameters is loaded into registers as specified for that SWI and a machine code instruction is then executed which jumps to the correct address for the required SWI which is specified by its number.
So how do we do this from BASIC?
Well BASIC has a keyword SYS (for SYStem) which simplifies things for us. The SYS keyword is followed by the number of the SWI we want to execute, and this is then followed by a comma separated list of the values to be loaded into each of the registers, starting with R0. In fact, BASIC has a few extra features which make this even simpler, but I'll describe these as we come to them.
For example, if we wanted to call SWI number 1234 with the value 23 in R0 and the value 100 in R1 we would use -
If the value 100 should have been in R2 instead of R1 then this would have been -
As practical example I shall use SWI number 0 to print a word to the screen. This SWI is extremely basic, it just PRINTs the ASCII character passed to it in R0. Normally, but not necessarily, this would be to the screen. So to PRINT a word we just have to call SWI 0 with a succession of letters in R0.
REM Print a word to the screen using SWI 0 PRINT SYS 0,72 SYS 0,101 SYS 0,108 SYS 0,108 SYS 0,111 PRINT END
The two PRINT statements before and after the SYS calls just PRINT blank lines.
The numbers passed to the SWI in R0 are the ASCII values of the letters of the word 'Hello', so this is what appears on screen. Not exactly sophisticated, but it serves to illustrate the method.
As I explained earlier BASIC has ways to simplify this. As you can imagine there are thousands of SWIs, and trying to remember their individual numbers would be impossible, and even worse, looking at a program later and attempting to work out what the SWI was doing from its number would be even more difficult. Most SWIs therefore can be called by name instead of just a number. The name of the SWI we have just used is OS_WriteC. SWIs fall into various groups and their names are usually in a similar format where the first section describes the group, in this case OS which means it's a basic Operating System SWI, followed by an underscore character and then a name which describes the action, WriteC being short for Write Character.
The previous program could therefore be written as -
REM Print a word to the screen using SWI 0 PRINT SYS "OS_WriteC",72 SYS "OS_WriteC",101 SYS "OS_WriteC",108 SYS "OS_WriteC",108 SYS "OS_WriteC",111 PRINT END
Note that the SWI name must be enclosed in double-quotes and it must be used precisely as specified, with exactly the same case.
As usual with BASIC the literal numbers passed as parameters could be suitable BASIC variables.
This isn't a very useful example, and it's also not very elegant or versatile as it can only print the characters written into the code. You wouldn't normally use this sort of routine for PRINTing text from BASIC, but while we're here I shall show a modified version. This doesn't use anything we haven't already covered in earlier lessons, so see if you can work out how it operates and perhaps modify it to print out different messages.
REM Sub routine to PRINT characters using OS_WriteC ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM text% 100 $text% = "Hello World" PROCprint_string(text%) END DEFPROCprint_string(ptr%) WHILE ?ptr% > 13 SYS "OS_WriteC",?ptr% ptr% = ptr% + 1 ENDWHILE ENDPROC
It is often necessary to pass parameters that can't be placed in a register, for example, strings. The way this is done is to place the data in memory and pass a pointer to it. You can do this for strings, but BASIC will do the work for you, so you can just use a string as a parameter and BASIC will substitute an appropriate pointer. This technique can be used with literal strings, that is, words enclosed in double-quotes, or string variables.
If you do want to pass a string to a SWI then normally it must be terminated with a Null character, which is a zero. BASIC strings are terminated with a Carriage Return or CR, ASCII 13. When you pass a BASIC string, either literal or a variable, BASIC inserts the correct terminating character, but if you do decide to use a direct pointer to a string you will normally need to insert the terminating zero.
CHR$ and ASC
Before I continue with more information on SWIs now seems an appropriate time to introduce two more keywords, ASC and CHR$, These are actually BASIC Functions, and work rather like the VAL and STR$ functions described in Part 2. ASC is used to convert an ASCII character to a number and STR$ converts a number in the range 32 - 255 to the equivalent character.
For example, as the ASCII value of a capital 'A' is 65, then
Would PRINT a letter 'A'.
The earlier program which used OS_WriteC would have been more easily read if I had used ASC instead of the actual ASCII numbers for the required letter. In this case it would become -
REM Print a word to the screen using SWI 0 PRINT SYS "OS_WriteC",ASC"H" SYS "OS_WriteC",ASC"e" SYS "OS_WriteC",ASC"l" SYS "OS_WriteC",ASC"l" SYS "OS_WriteC",ASC"o" PRINT END
CHR$ is especially useful because it can be used for introducing unusual characters into strings. The procedure in the third program just prints letters until it reaches the terminating character of the BASIC string, which is a CR or ASCII 13. If we had wanted to split our text over two lines we would have to introduce a CR character (13), which means 'move to the start of the line' and a linefeed character (or LF, ASCII 10) which means 'move down one line'. We would also need to find another means of identifying the end of the text in the procedure which prints it as otherwise it would stop when it found the first CR. We can do this by using a 'null' character as the terminator. The new program then becomes -
REM Sub routine to PRINT characters using OS_WriteC ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM text% 100 $text% = "Hello"+CHR$(13)+CHR$(10)+"World"+CHR$(13)+CHR$(10)+CHR$(0) PROCprint_string(text%) END DEFPROCprint_string(ptr%) WHILE ?ptr% > 0 SYS "OS_WriteC",?ptr% ptr% = ptr% + 1 ENDWHILE ENDPROC
You can see how the additional characters are just 'added in' to the string. I have also put a CR and LF at the end of the text so that any further text will begin on a new line.
You will see from this last section how we can introduce the '0' terminator required by most SWIs onto the end of a BASIC string. In fact our procedure for printing text using OS_WriteC is really redundant as there is a SWI which does does exactly the same thing, OS_Write0. Note that the last character of this name is a zero, not a capital O. This SWI does what you would expect, you call the SWI with a pointer to the text in R0 and it prints characters to the screen until it finds the terminating null. By substituting a call to OS_Write0 the program could be simplified -
REM Routine to PRINT characters using OS_Write0 ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM text% 100 $text% = "Hello"+CHR$(13)+CHR$(10)+"World"+CHR$(13)+CHR$(10)+CHR$(0) SYS "OS_Write0",text% END
Getting information back
So far the SWIs I have used have been extremely simple, and just required information to be passed to the OS. However many SWIs require a two way exchange of information. Sometimes this is done by passing the data back in registers, but if this is not appropriate, for example, if the data is a string, then it is passed back in a block of RAM with a register holding a pointer to it.
As you might expect BASIC has a method of accessing these returned values. After the list of data to be passed to the SWI you add the keyword 'TO' and then follow this with a comma separated list of BASIC variables and the returned values in the registers are assigned to these variables. R0 is assigned to the first variable, R1 to the second, and so on.
I have explained that all SWIs are given a number, and many also have a name. There are SWIs to convert names to numbers and vice versa. The one to convert a name to a number is OS_SWINumberFromString. The specification for this says that you call the SWI with a pointer to the SWI name in R1 and on exit R0 will hold the number of the SWI. Translated into BASIC this becomes -
DIM text% 100 $text% = "OS_Write0"+CHR$(0) SYS"OS_SWINumberFromString",,text% TO number% PRINT number%
If you Run this program you will see that OS_Write0 is SWI number 2.
The SWI to convert from a number to a string is OS_SWINumberToString. This requires the SWI number to be passed in R0, a pointer to a suitable buffer for the string to be returned in R1, and the length of this buffer in R2. The SWI number will then be placed in the buffer and R2 will hold the length of this string. This gives us a bit more scope for writing an interactive program which will ask the user for a number and then display the SWI name associated with that number (if any).
REM Program to convert a SWI number to a name ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM name% 100 REPEAT INPUT "Enter SWI number - "swi_num% IF swi_num% >= 0 PROCconvert(swi_num%) UNTIL swi_num% < 0 END : DEFPROCconvert(number%) SYS "OS_SWINumberToString",number%,name%,100 TO ,,length% name%?length%=13 PRINT "Name is "+$name% ENDPROC
I'm not going to describe the workings of this program in detail, by now you should be able to work this out for yourself, but I will explain a couple of things about the way that OS_SWINumberToString is used in PROCconvert.
You will see that the SWI is called, as required, with the number to be converted, number%, in R0, a pointer to the buffer for the returned string in R1, actually name% which is an integer array we had previously DIM'ed, and the length of this buffer in R3. When the call returns we aren't interested in the contents of R0 or R1, but we do need to know the contents of R2, which is the length of the string, so we assign this to the variable length%.
We need to know the length of the string because the name returned will be terminated with a null character and if we want to use PRINT to display it we must substitue the character that BASIC requires, a CR or 13, for this. As we know the length of the string we can use the indirection operator '?' to insert a CR after its end. In this particular instance you could use OS_Write0 to print the name instead, and you might want to try modifying the program to do this.
If you use this program you will notice that a lot of numbers return the name 'User'. This doesn't mean that there are a lot of SWIs with this name, but that this number hasn't isn't used.
Next issues article will continue exactly where this one left off.