lmr_task.bas (8595Bytes)
Multi-Tasking BASIC programming on the Picaxe 28X1
Calm down dear, it’s only a BASIC program.
This is a work in progress but I thought I would share it at this time both as a way of giving to the LMR community and as a way of soliciting input from more experienced roboteers and PICAXE programmers.
I am rather new to the Picaxe and its dialect of BASIC; however the code I have seen always seems to be performing operations and then pausing for some period – during which time the program is unable to do anything else. For example, when a servo or motor is commanded to move the program pauses for a while while the actuator moves into position, or an SRF005 is triggered and you have to wait 10 milliseconds before the next triggering to give the device time to recharge. So why not do something else while a servo is turning?
As all we can program in is BASIC we are a bit limited in what we can do, and with it being a PIC we do have limited resources with which to do it, so there was no way that we are getting a pre-emptive multi-threaded processing model. I came up with a framework where little tasks are added to a queue and the main program loop pulls the tasks from the queue and processes them. Each task that is added to the queue indicates when it should be executed by providing a nominal delay before execution, and what should be executed by identifying a subroutine to call.
With this being written in BASIC, the code for the ‘things to do’ are defined in sub-routines, each of which is identified as a task by assigning it a number and adding the appropriate call to an ON … GOSUB statement in the main processing loop. The dynamic nature of the system rules out the use of normal variables for data storage. Fortunately the28X1 part contains 126 bytes of scratchpad from where we can carve out the space we need for our data storage and control structures.
The task queue is maintained using two data structures, the first provides control of the queue and contains two values, the first gives the offset to the next item in the queue that will require processing, the second item contains the offset to the first item in a list of free tcbs (Task Control Block). The layout of the scratchpad is defined using symbol statements to define the offsets from some ‘base’ offset – which is the offset to the start of the structure.
symbol tcb_control_base = scratchspace_base + scratchspace_space
symbol tcb_control_active = 0 ’ root of active tcbs
symbol tcb_control_freelist = 1 ’ root of free tcbs
A tcb contains information needed to define some processing that needs to be done
symbol tcb_base = tcb_control_base + tcb_control_space
symbol tcb_next = 0 ’ link to next tcb (active or free)
symbol tcb_countdown = tcb_next + 1 ’ ticks remaining until actioned
symbol tcb_action = tcb_countdown + 1 ’ the action to take
symbol tcb_arg1 = tcb_action + 1 ’ the first argument to pass (b0)
symbol tcb_arg2 = tcb_arg1 + 1 ’ the second argument to pass (b1)
In addition to the offsets from the ‘base’ value, we defined the number of instances of the structure we need space for and from that we can calculate the total space needed.
symbol tcb_size = tcb_arg2 + 1 ’ the number of bytes in a tcb
symbol tcb_count = 8 ’ the number of tcbs to allocate
symbol tcb_space = tcb_count * tcb_size ’ the number of bytes for all tcbs
So here we are catering for a maximum of 8 outstanding tasks
There are four subroutines defined that maintain the task list and the status of the tasks.
tcb_init:
This must be called to initialise the control information for the queue
tcb_new:
This is called to allocate a new tcb and insert it into the active queue
tcb_free:
This is called to remove the current active task from the active queue and put it into the free queue.
tcb_clock_tick:
This code needs to be called to indicate the passing of time, which will make the tasks at the head of the active queue eligible for execution.
So how do we use this?
As an example, I have defined a task that will toggle an LED on a specified port.
First of all we need to define a value for the action. This is related to the position in the ON … GOSUB line in the main processing loop
symbol toggle_output_action = 0
Subsequently we need to add an initial task to the queue. To do this we set up a few values in general variables and call the tcb_new subroutine.
main:
gosub tcb_init
b0 = 0 : b1 = toggle_output_action : b2 = 0 : b3 = 10 : gosub tcb_new
where b0 specifies the delay before the task is performed, b1 indicates the action to be performed, b2 and b3 define values to be provided (in b0 and b1) when the subroutine is called.
We need to make changes to the main processing so it will call the subroutine when the action is invoked. In this case the action number is zero so it needs to be the first label in the list.
ptr = b0 + tcb_action
b4 = @ptrinc ’ action
b0 = @ptrinc ’ arg1
b1 = @ptrinc ’ arg2
==> on b4 gosub toggle_output
and finally we need to write the code for the subroutine. In this case we toggle the output port and then add a new task that will be invoked after ‘b1’ ticks.
toggle_output:
’ toggle output port
’ b0 - arg1 from tcb
’ b1 = arg2 from tcb
toggle b0
b2 = b0
b3 = b1
b0 = b1: b1 = toggle_output_action : gosub tcb_new
return
So there we have it. When the code is run it will add a single task to the queue and when the countdown expires it calls the toggle_output subroutine which toggles the output and adds a new task to the queue. The new task is identical to the one that just completed. Because of this regeneration the program will process continuously as there will always be a task in the queue.
You might be thinking this is a lot of code and bother to make an LED flash, and you are right, but it is dead easy to get 8 LEDs winking ‘as if by magic’.
b0 = 0 : b1 = toggle_output_action : b2 = 0 : b3 = 1 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 1 : b3 = 2 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 2 : b3 = 4 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 3 : b3 = 8 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 4 : b3 = 1 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 5 : b3 = 2 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 6 : b3 = 4 : gosub tcb_new
b0 = 0 : b1 = toggle_output_action : b2 = 7 : b3 = 8 : gosub tcb_new
Given this framework it is possible to defined subroutines that control servos, motors or read values from sensors on a regular basis. It should be a simple job to define a new task in a similar vein to the output toggling that checks the status of input switches that would either act upon the switches itself or add a new task to the queue to pass control to a different subroutine, e.g. a bumper switch would add a task to invoke a motor shutdown.
From my experience I would suggest that each of the subroutines needs to implement some sort of state machine, using the b2/b3 arguments to the action to control the execution within a subroutine and queuing actions and arguments that allow the control of execution to pass to other subroutines, e.g. it is possible to implement scanner that starts servo motion task and when notified by the servo code that the servo has moved it can initiate a distance reading from an SRF005. The SRF005 code takes the reading, queues a task that completes in 10ms that sets the SRF005 state to ‘recharged’ and another task that informs the scanner code that the reading has been taken so it can process it.
A Warning
The code will process all tasks that have a countdown of zero before ‘ticking’ the clock. If a subroutine creates any tasks with a delay of zero then those tasks will be processed before the next tick of the clock. It is therefore possible to stop the processing of other tasks by continuously adding new 0 delay tasks.
Fatal Error
The code detects two conditions that will stop the processing and cause the program to enter its fatal error processing, the first is where the task queue is empty – and if there are no tasks there is nothing to do, the second is when there are no free TCBs. It’s easy enough to increase the space allocated for them.
symbol error_bang_and_its_gone = 1
symbol error_out_of_memory = 2
The fatal error processing will flash an LED on the defined port. If you count the flashes you can see what the problem is.
Notes
My original plan was to use the timer on the PIC to generate the timed events, the problem with this is that the timer is needed when the PIC is outputting the servo pulses. In the end couldn’t see how to generate these interrupts internally so the main loop actually pauses for 10ms before re-processing its queue, which is less than ideal. Given suitable external stimulus (perhaps a timer circuit?) it would be possible to clock the system in the manner in which it was intended to be clocked. An alternative would be an external clock device that we can read to determine how long we need to pause for to make up appropriate delays before processing the next item from the queue. The lack of any accurate timing means that the current code does little more than promise to execute the tasks in the correct order with at least a 10ms delay.
Remember that most of the symbols defined are offsets to some piece of information, not a value that is being used.
I don’t know if anyone else will find this useful, but I shall develop further code using this framework as/when I need it.
Questions?