About Subroutines
A subroutine is a small program inside of the main program. Subroutines are typically used when a task needs to be repeated several times in different parts of the main program.
There are two main uses for subroutines:
- Keeping programs neat and easy to read
- Reducing the size of programs by allowing common sections of code to be reused.
When the microcontroller comes to a subroutine it saves its location in the current program before jumping to the start of, or calling, the subroutine. Once it reaches the end of the subroutine it returns to the main program, and continues to run the code where it left off previously.
Normally, it is possible for subroutines to call other subroutines. There are limits to the number of times that a subroutine can call another sub, which vary from chip to chip:
Microcontroller Family | Instruction Width | Number of subs called |
---|---|---|
10F*, 12C5*, 12F5*, 16C5*, 16F5* |
12 |
1 |
12C*, 12F*, 16C*, 16F*, except those above |
14 |
7 |
18F*, 18C* |
16 |
31 |
These limits are due to the amount of memory on the microcontroller which saves its location before it jumps to a new subroutine. Some GCBASIC commands are subroutines, so you should always allow for 2 or 3 subroutine calls more than your program has.
On 16F chips, the program memory is divided into pages. Each page holds 2048 instructions.
If the program jumps from code on one page to code on another, the compiler has to select the new page. Having to do this
makes the program bigger, so try to avoid this.
To keep jumps between pages down, GCBASIC imposes a rule that each subroutine must be entirely within one page, so that only
jumps to other subroutines require the page selection.
As an example, say you have two pages of memory, each 2048 instructions (words) long.
If you have a main sub that is 1500 words, and four other subroutines each 600 words long, your total program size would be
3900 words and you might expect it to fit into the 4096 words available.
The problem though is that once the main routine takes 1500 words from page 1, nothing else will fit after it. Three of the
600 word subroutines would fit onto page 2, but that leaves one 600 word subroutine that will not fit into the 500 left on
page 1 or the 200 left on page 2.
If you want to reduce the chance of this happening, the best option is to keep your subroutines smaller - move anything out
of the main routine and into another one - this will resolve memory page constraints.
Atmel AVR microcontrollers have no fixed limit on how many subroutines can be called at a time, but if too many are called then some variables on the chip may be corrupted. To check if there are too many subroutines, work out the most that will be called at once, then multiply that number by 2 and create an array of that size. If an out of memory error message comes up, there are too many calls.
Another feature of subroutines is that they are able to accept parameters. These are values that are passed from the main program to the subroutine when it is called, and then passed back when the subroutine ends.
Using Subroutines
To call a subroutine is very simple - all that is needed is the name of the sub, and then a list of parameters. This code will call a subroutine named "Buzz" that has no parameters:
Buzz
If the sub has parameters, then they should be listed after the name of the subroutine. This would be the command to call a subroutine named "MoveArm" that has three parameters:
MoveArm NewX, NewY, 10
Or, you may choose to put brackets around the parameters, like so:
MoveArm (NewX, NewY, 10)
All that this does is change the appearance of the code - it doesn’t make any difference to what the code does. Decide which one meets your own personal preference, and then stick with it.
Creating subroutines
To create a subroutine is almost as simple as using one. There must be a
line at the start which has sub
, and then the name of the subroutine.
Also, there needs to be a line at the end of the subroutine which reads
end sub
. To create a subroutine called Buzz
, this is the required
code:
sub Buzz 'code for the subroutine goes here end sub
If the subroutine has parameters, then they need to be listed after the
name. For example, to define the MoveArm
sub used above, use this
code:
sub MoveArm(ArmX, ArmY, ArmZ) 'code for the subroutine goes here end sub
In the above sub, ArmX
, ArmY
and ArmZ
are all variables. If the call
from above is used, the variables will have these values at the start of
the subroutine:
ArmX = NewX ArmY = NewY ArmZ = 10
When the subroutine has finished running, GCBASIC will copy the values
back where possible. NewX
will be assigned to ArmX
, and NewY
will be
assigned to ArmY
. GCBASIC will not attempt to set the number 10 to ArmZ
.
Controlling the direction data moves in
It is possible to instruct GCBASIC not to copy the value back after the subroutine is called. If a subroutine is defined like this:
sub MoveArm(In ArmX, In ArmY, In ArmZ) 'code for the subroutine goes here end sub
Then GCBASIC will copy the values to the subroutine, but will not copy them back.
GCBASIC can also be prevented from copying the values back, by adding
Out
before the parameter name. This is used in the EEPROM reading
routines - there is no point copying a data value into the read
subroutine, so Out
has been used to avoid wasting time and memory. The
EPRead routine is defined as Sub EPRead(In Address, Out Data)
.
Many older sections of code use #NR
at the end of the line where the
parameters are specified. The #NR
means "No Return", and when used has
the same effect as adding In
before every parameter. Use of #NR
is
not recommended, as it does not give the same level of control.
Using different data types for parameters
It is possible to use any type of variable a as parameter for a
subroutine. Just add As
and then the data type to the end of the
parameter name. For example, to make all of the parameters for the
MoveArm
subroutine word variables, use this code:
sub MoveArm(ArmX As Word, ArmY As Word, ArmZ As Word) ... end sub
Optional parameters
Sometimes, the same value may be used over and over again for a parameter, except in a particular case. If this occurs, a default value may be specified for the parameter, and then a value for that parameter only needs to be given in a call if it is different to the default.
For example, suppose a subroutine to create an error beep is required. Normally it emits ! 440 Hz tone, but sometimes a different tone is required. To create the sub, this code would be use:
Sub ErrorBeep(Optional OutTone As Word = 440) Tone OutTone, 100 End Sub
Note the Optional
before the parameter, and the = 440
after it.
This tells GCBASIC that if no parameter is supplied, then set the
OutTone
parameter to 440.
If called using this line:
ErrorBeep
then a 440 Hz beep will be emitted. If called using this line:
ErrorBeep 1000
then the sub will produce a 1000 Hz tone.
When using several parameters, it is possible to make any number of them optional. If the optional parameter/s are at the end of the call, then no value needs to be specified. If they are at the start or in the middle, then you must insert commas to allow GCBASIC to tell where the optional parameters are.
Overloading
It is possible to have 2 subroutines with the same name, but different parameters. This is known as overloading, and GCBASIC will automatically select the most appropriate subroutine for each call.
An example of this is the Print routine in the LCD routines. There are actually several Print subroutines; for example, one has a byte parameter, one a word parameter, and one a string parameter. If this command is used:
Print 100
Then the Print (byte) subroutine will be called. However, if this command is used:
Print 30112
Then the Print (word) subroutine will be called. If there is no exact match for a particular call, GCBASIC will use the option that requires the least conversion of variable types. For example, if this command is used:
Print PORTB.0
The byte print will be used. This is because byte is the closest type to the single bit parameter.