Multiple tasking question

Discussion about the ZBasic language including the System Library. If you're not sure where to post your message, do it here. However, do not make test posts here; that's the purpose of the Sandbox.
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Multiple tasking question

Post by twesthoff »

This is the first time I have actually tried to use a second task, so am not sure I understand it enough.

I just tried adding a second task to a previously working program. The program does a bunch of stuff, and every so often needs to send a sequence of DTMF digits using a MT8888 chip.

I have a Sub SendDigit(digit) that when called waits in a loop for the chip to be ready to send, then sends the digit. The chip is busy for 100ms after it gets the digit while it sends the digit for 50ms and waits 50ms for the gap between digits. This sub works fine.

My problem is, that sending multiple digits caused the main program to have to wait for up to 100ms (for each digit) while the DTMP chip is busy. I'd like to allow the main program to continue its processing, so I thought I would add a task to send the digits in the background.

The idea is that I would make a Queue to hold the digits to be sent. When the main program needed to send a digit, it would add the digit to the queue instead of calling Sub SendDigit.

The second task would continuously check the queue, and if data were present get the byte and then call SendDigit. When not sending digits, I would like this task to use as few resources as possible to allow the main program to run as fast as possible.

Here is my task:

Code: Select all

Dim DTMFtaskStack(1 to 100) as Byte    
Dim DTMFq(40) as byte     


Sub DTMFsend ()     'Task to send DTMF digits in the background  
  Dim DTMFbyte as Byte
  Do
    If StatusQueue(DTMFq) Then          'Any digits available?
      Call GetQueue(DTMFq, DTMFbyte, 1) 'Get the digit
      Call SendDigit(DTMFbyte)          'Send the digit
    End If 
    Sleep(0.120)                        'Give up task time?, best value?
  Loop  
End Sub
As shown, it works OK. But if I make the Sleep value <100ms, depending on the value, some of the digits don't send, or if short enough, none are sent. Since SendDigit waits for the DTMF chip to be ready, I thought the sleep value should not matter. I was putting the Sleep in in the hope that it would give more time to the main program.

I have read the app notes and the manual pages, but I must be missing something subtle...
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

If SendDigit() waits in a loop for the DTMF chip to be ready, this will still result in unnecessary delays. It would probably be better to check for the conjunction of two conditions: the DTMF chip is ready AND there is a digit to send.

As for the delay value, zero should work fine. If it appears that smaller values don't work it may be that the queue is being filled by the provider.
How are you adding digits to the queue?
- Don Kinzer
stevech
Posts: 715
Joined: 22 February 2006, 20:56 PM

Post by stevech »

I'll guess that the handshaking with the DMTF chip's ready/busy isn't working correctly. Perhaps you can post that code and the nature of the I/O interface?

Not shown, but no doubt the other code calls OpenQueue() to initialize the shared queue.

A nit is that instead of the sleep(), you can reduce the latency in detecting queue not empty by using
GetQueue(queue, var, count)
which will suspent until it can return <count> number of bytes.
or
GetQueue(queue, var, count, timeLimit, timeoutFlag)

where the timeout flag will cause it to return before <count> number of bytes are available.

I've used these for inter-task communications.
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

I'll try to answer both of you.
Stevech, The handshaking has been working perfectly for several years in the main program, I didn't change any code, but I will see if I can check it as it acts like it is not waiting for the chip to be ready.

Yes, I did start the queue and the task at the beginning of the main program.

Don, I was hoping the second task would check to see if there was any data, and if not go to sleep and give up its time to the main program. If there is data in the queue, then the second task could wait for the dtmf chip if necessary thinking that the main program would still be running, however a bit slower while the dtmf task was doing its job. Does sleep() cause the second task to give up its time? Maybe this is not the right way to do what I want to accomplish.

The queue could contain 10 or more digits waiting to be sent.

Here is the code I am using to fill the queue:

Sub QueueDigit(ByVal d as Byte)
'Puts Digit in DTMFq to be sent by the DTMFsend task
If GetQueueCount(DTMFq) <= GetQueueBufferSize(DTMFq) Then 'Space available
Call PutQueueByte(DTMFq, d)
Else
Call DebugPoint("Qf=" & CStr(GetQueueCount(DTMFq))) 'Report Queue ERROR
End If
End Sub
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:I was hoping the second task would check to see if there was any data, and if not go to sleep and give up its time to the main program.
That is exactly what you accomplish by using Sleep(). You could implement the conjunction by having SendDigit() also call Sleep() if the DTMF chip is busy thus allowing the main task to do something useful while the DTMF chip is busy.
- Don Kinzer
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

Don,
Yes, I think that is what I had in mind, but did not know if I needed that or not. I could make SendDigit the task and put the queue reading in it too.

If I didn't have the sleep, would the two tasks share the cpu roughly 50/50?
would my main program lose about 50ms of cpu time?

Do I pick a sleep value that is the largest value I can deal with, for example if I said Sleep(0.1) if I just missed the chip being ready, then there would be a gap between digits of at least 100ms + 50ms the chip already does? The idea is to make a string of digits 50ms on and 50ms off, etc.

By the way, did you get my email about the BusRead?
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:If I didn't have the sleep, would the two tasks share the cpu roughly 50/50? Would my main program lose about 50ms of cpu time?
Yes, assuming that there are no other calls that implicitly or explicitly suspend the task.

Recall that the "time slice" for each task is 1.95mS. Exclusive of task locking and calls that consume long periods of time, that's the longest that any task will run before the VM allows the next task to run. Consequently, a task that is looping waiting for an external device does so in 1.95mS segments.
twesthoff wrote:Do I pick a sleep value that is the largest value I can deal with [...]
Sleep() calls generate only approximate timing. A task can end up being suspended for longer than the specified time.
twesthoff wrote:By the way, did you get my email about the BusRead?
Yes. I've been thinking about a solution. I'll reply separately.
- Don Kinzer
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

If there are main and 1 other task running, and the 2nd task does a sleep(0.020), does the other task get about 20 ms worth of time for itself?

I'm trying to figure out if my SendDigit wait loop should have a Sleep of 5ms, 10ms, 0r 50ms, to get the most time for the main program.
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:If there are main and 1 other task running, and the 2nd task does a sleep(0.020), does the other task get about 20 ms worth of time for itself?
Effectively, yes. On each RTC tick, the VM will suspend the first task and attempt to find another task that is ready to run. In your example, the sleeping task will not be ready so the first task will be run for another time slice and the process will be repeated until the second task is ready to run.

It will probably work best not try to effect the timing that you need by using Sleep(). Rather, in each task when no more work can be done then call Sleep(0) to force a task switch and let the next task run.
- Don Kinzer
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

I think I have figured out what my multitasking problem was. Since there is now 2 tasks that need access to the pseudo data bus (port) that is being used to get the status of the DTMF chip in task 2, and the main task is checking other chips that share the same data bus port, the tasks are most likely being switched in the middle of a data bus access. So one task may change the bus data or start a read or write, and the next task either changes it, or reads what was partially done by the other task.

In my single task program, this could not happen as only one routine could have access to the bus at a time.
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:[...] the tasks are most likely being switched in the middle of a data bus access.
The BusRead() and BusWrite() calls are atomic with respect to task switching. By this I mean that once a task calls either of these routines it is guaranteed to finish before returning. Once the routine returns, the calling task may be suspended and a new task activated.
- Don Kinzer
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

That would solve a lot of problems for me.

Could I add to my present code and force it to be atomic using LockTask/UnlockTask? I have to read about them some more...
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:Could I add to my present code and force it to be atomic using LockTask/UnlockTask?
To reiterate, BusRead() and BusWrite() calls are atomic. No additional effort is required to prevent two tasks' use of those calls from interfering with each other.

Since we're discussing atomicity, it is useful to know that each VM instruction is atomic. That is, a task switch will only occur "between" VM instructions. You can see the VM instructions generated by the compiler by enabling the production of a listing file (see the --list option in section 6.2 of the ZBasic Language Manual). Although the VM instruction set isn't documented, it is fairly easy to deduce the meaning of each instruction from its context.

Consider this simple program:

Code: Select all

Sub Main&#40;&#41;
  Dim data&#40;1 to 10&#41; as Byte
  Call BusRead&#40;0, data, 10&#41;
End Sub
The listing file produced contains the following excerpt:

Code: Select all

            Sub Main&#40;&#41;
0011 0e0a        ADDSP_B        0x0a &#40;10&#41;
            	Dim data&#40;1 to 10&#41; as Byte
0000 +0    var&#58;data &#40;10&#41;
            	Call BusRead&#40;0, data, 10&#41;
0013 1b0000      PSHI_W         0x0000 &#40;0&#41;
0016 0d0000      PSHR_A         bp+0
0019 1b0a00      PSHI_W         0x000a &#40;10&#41;
001c 1a01        PSHI_B         0x01 &#40;1&#41;
001e feb0        SCALL          BUS_RD
            End Sub
0020 06          RET
Here, you can see the instructions generated to implement the call to BusRead(). The four instructions that precede the SCALL instruction place parameters on the stack. The SCALL instruction is a "system call", in this case for the BUS_RD system call. A task switch can occur at the boundary between any of these instructions but not in the midst of any of them.
- Don Kinzer
twesthoff
Posts: 247
Joined: 17 March 2006, 6:45 AM
Location: Fredericksburg, VA

Post by twesthoff »

Thanks for the explanation Don. The BusRead will work in some parts of of my program, but not in the Fifo portion. Also there is a separate chip select pin for the DTMF chip that must be held low during the BusRead which takes two instructions.

Some of this is due to a poor circuit design that I inherited. I have decided to make a new board that will use the '1281 part and be easier to use, but that will take a while to do.

For now I think will have to add the LockTask before the code that must execute without interruption (in both tasks), and a Sleep right after it. I think that should solve the problem, now that I understand what is happening better.
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

twesthoff wrote:For now I think will have to add the LockTask before the code that must execute without interruption (in both tasks), [...].
Another technique for serializing access to a resource is to use a semaphore. This is addressed briefly in Mike's application note "Sharing Data Between Tasks", AN-210 and also described in the System Library Manual entry for the Semaphore() function.

The general idea is to have each task that wants to use a particular resource, in this case the BusRead/BusWrite interface, wait until a semaphore can be successfully acquired before proceeding. The outline of the code would be as follows:

Code: Select all

' global semaphore variable
Public BusSemaphore as Boolean

' each user of the bus interface should do the following

  ' wait until the bus interface is available
  Do Until Semaphore&#40;BusSemaphore&#41;
  Loop

  ' prepare for and perform BusRead/BusWrite operations, e.g.
  Call BusRead&#40;a, b, c&#41;

  ' signal that the bus interface is now available by clearing the semaphore
  BusSemaphore = False
- Don Kinzer
Post Reply