Multi-Tasking BASIC programming on the Picaxe 28X1

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?




wow, that’s intense

So what would be a practical application for this multi-tasking framework?

 

I have some experience with

I have some experience with this from when I was coding in assembly. Instead of a queue to execute code when you needed a pause, we simply rearranged code to make it more efficient. For example:

Code to use SRF05 and have an LED indicate (light up for 1 second) when it is reading distance.

Old pseudo code: (time to execute code (questimate)
send pulse (2ms)
wait (20 ms)
receive pulse and calculate distance (5ms)
light LED (2ms)
wait (1 second)
turn off LED (2ms)

TOTAL 1031ms

New pseudo code:
light LED (2ms)
send pulse (2ms)
wait (20ms)
receive pulse and calculate distance(5ms)
smaller wait (973ms)
turn off LED (2 ms)

TOTAL 1004 ms

since sending the pulse takes a few miliseconds a smaller wait is needed because it consumes some of the wait when you originally had the LED go on and off.

I would love to see more work done with your idea. My only concern is that the overhead of queuing is > the processor time saved. You would almost have to look at how many miliseconds it takes to execute the overhead versus the time saved.

Well I am still thinking about this

It should be possible to have multple things ‘going on’ at the same time. You have a navigate task that spawns a task to scan using a servo and an SRF. That task in turn gets the servo code to move the servo and when it says it is position (after a suitable delay) the scanner task starts an SRF task that triggers the SRF and queues another task to set the SRF status to Ready after 10ms. While that is happening it can also have another task that monitors switches (e.g. bumper switches) on every iteration of the main loop. In theory the bumper switch task would call the Navigation task so it can decide what to do. I say ‘in theory’ because I have only got as far as having a scanner task that positions the servo and then triggers the SRF.

My aim would be to support similar navigation as Ftrits’ code. I don’t see why it shouldn’t work, but goodness knows how long it will take :slight_smile:

Picaxe BASIC isn’t the fastest

Yes, the speed is likely to be a problem but I think that the queue management is relatively light-weight. It is likely to be more of a problem getting back to the main loop quickly and often. An external clock would improve the synchronicity, at the moment about all that is guaranteed is the order of execution. In a way it isn’t about doing things faster but doing more in the time available.

I shall keep working on this for a while yet if only as an exercise for the mind. New toy and all that (if only it had some wheels).

I think it is a good input.i

I think it is a good input.

i always start up with the one-step-approach, but then I include everything in sub-routines that I fire instead of pause’s

Example: https://www.robotshop.com/letsmakerobots/node/254