[TUT][C] Multi-tasking tutorial part 1

Go To Last Post
146 posts / 0 new

Pages

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm happy that you've gained something from it. Using the bit_mask table is a bit of a throwback to when i first wrote the code on a 8051 processor. With the AVR you could flip a coin vs using shift operators. With an ARM processor, using a shift operator would be more efficient.

I've used the framework on a number of commercial projects spanning nearly 20 years. There's many little boxes ticking away happily around the world.

It's good to get feedback from readers, thanks.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Time to update to __flash and drop the pgm_read*() stuff perhaps? ;-)

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Agreed. 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have some useful information on realtime systems from my university;

 

Some appropriate lecture slides (this presents one way of "simultaneous" execution on a single core processor, we used AVRs in the labs).

 

http://www.sm.luth.se/csee/courses/d0003e/lectures/lecture4.pdf

 

And a light weight multi-threading kernel, tinythreads. However, some of the methods are unimplemented - that's for the students to do.

 

http://www.sm.luth.se/csee/courses/d0003e/labs/tinythreads.h

http://www.sm.luth.se/csee/courses/d0003e/labs/tinythreads.c

 

And to actually use the kernel, use "spawn" from main on methods that do not terminate. As shown here, example:

 

http://www.sm.luth.se/csee/courses/d0003e/labs/mytest.c

 

What makes the threads context switch is inserting a call to "yield", somewhere in the code of the spawned methods, or why not from an interrupt handler (ISR) written in tinythreads.

If you want to know the implementation of the yield method - which triggers the context switching - then you'll have to do some coding yourself (HINT: It's four lines of code, including turning on and off interrupts).

Yield shall enqueue the current thread and dispatch the one in the waiting queue. (PM me if you're not up for my bullshit, I'll show you)

sol i sinne - brun inne

Last Edited: Fri. Jan 2, 2015 - 05:16 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If you yield within an isr, is that not preemption?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Kartman wrote:

If you yield within an isr, is that not preemption?

 

Oh, yeah sure. Right now I've hooked my Arduino up to an LED (I'm strapped for parts, can't make anything more advanced at the moment), that one blinks on/off every 300ms, and the on-board LED blinks on/off every 200ms, both using very simple endless loops with _delay(). The interrupt comes from the WDT set to timeout every 32ms, works flawlessly to the naked eye.

sol i sinne - brun inne

Last Edited: Wed. Jan 7, 2015 - 10:54 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

As a side note (nothing to do with multitasking, but interrupt handlers), depending on your system and what the requirements are, you can get away with just runnig your code in the interrupt handlers (: That way you can sleep in the main-loop, saving energy!

sol i sinne - brun inne

Last Edited: Sat. Jan 3, 2015 - 02:50 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

This tutorial was not meant to be the last word in multitasking - as i said up front, it is an example of a simple, cooperative tasker. There are a number of ways to achieve the same or similar results.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Of course! (:

sol i sinne - brun inne

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

@Tickstart

 

Why don't you write up your approach as a tutorial?

#1 Hardware Problem? https://www.avrfreaks.net/forum/...

#2 Hardware Problem? Read AVR042.

#3 All grounds are not created equal

#4 Have you proved your chip is running at xxMHz?

#5 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand."

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Brian Fairchild wrote:

@Tickstart

 

Why don't you write up your approach as a tutorial?

It's not an impossibility, but I'm not the original author of this program. Not that it's copyrighted or anything but since I didn't write it myself (and I'm really no C programmer) I might not be able to answer those really tricky questions. We'll see, until then all information is in those slides and in the submitted code, only thing missing is the implementation of yield which I can help you with. The mutexes I haven't looked into since the threads I've run haven't shared common resources, but I have the code for those aswell.

sol i sinne - brun inne

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thanks for such a wonderful tutorial....waiting eagerly for the 2nd part...

 

Saikat

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Can I call another task from one task? Will it make create any problem?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

sktnandy wrote:

Can I call another task from one task?

 

Why would you want to do that?

 

I'd suggest that if you find yourself needing to do it then you have partitioned your tasks wrongly.

#1 Hardware Problem? https://www.avrfreaks.net/forum/...

#2 Hardware Problem? Read AVR042.

#3 All grounds are not created equal

#4 Have you proved your chip is running at xxMHz?

#5 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand."

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'd suggest that if you find yourself needing to do it then you have partitioned your tasks wrongly.

My thoughts to. In multi-tasking it is often the case that you have one task that "does a job for" another task but rather than CALL it you might typically have the "worker" task block on a mutex or semaphore then, when the parent task wants the job done it release the shared object that triggers the other into life. Anotther way is for the parent to post a request into a message queue of the child task and when it receives and "OK, go for it" message it springs into life. This is quite different to synchronously calling functions in one task from another. You have to start thinking about the problem in a "different way" when you try to share work across threads and tasks in a multi-tasking system.

 

At the end of the day think about it just like you get to write three or five (or however many) different main()s in a single program. Each "does it's own thing". But a bit like when in Windows your word processor sends a document to the print queue along with another document already waiting to be printed from a spredasheet, sometimes the main()'s "talk" to each other. That's at the highest level of course but really the issue is pretty similar.

 

So in what context do you see the need for one task to "call" another?

 

Of course there are things that both tasks might call - I bet they might both be tempted to call printf() but that does not "belong" to either task really - it's a shared resource hey can both use. Of course that raises another issue. If one does printf("hello") and one does printf("goodbye") at the same time how do you prevent the user seeing "hegloodlyeo" ? (or maybe that's what you want?). This is really where a mutex (or semaphore if more than 2 tasks) might come into use. A mutex (mutual exclusion) ensures that only one task can "own" a resource at a time. So both programs call get_mutex(printf); printf("hello"/"goodbye"); release_mutex(printf). The one that "gets there first" gets control and can complete his printf() then he release the lock. Meanwhile the other task is "stuck" inside get_mutex() waiting for it to become available. When the first task calls release_mutex() that allows the get_mutex() to then complete and he can then do his printing. So the user sees "hellogoodbye" or "goodbyehello" depending on which got there first.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thanks brian and clawson.....I am not willing to do semaphore or something like that because it will get more complicated. I will re-code the tasks so that there is no need to call one from another. 

And thank you clawson for such a wonderful explanation.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I re-coded it so that there is no need to call one task from other. But I am getting a strange problem after running it a while. It is getting stuck somewhere and not coming out. How to figure it out where it is getting stuck? 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

How to figure it out where it is getting stuck? 

It kind of depends what "stuck" really means - you meant the task switching preemption actually stops or simply that one of the tasks goes into an infinite loop?

 

Anyway an OCD debugger (JTAG/debugWire/PDI/whatever) is usually the best for this kind of thing or failing that (if not too much external stimulation is involved) the simulator in Studio. Run the code until it is "stuck" then simply break execution and find out where it is. Maybe follow it into the task preemption (assuming that's till going) and watch it as it returns into each task. Is one (or all even?) "stuck" in a loop waiting for something to happen that never will?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

It seems one of the task is going into an infinte loop. This problem is happening randomly like sometimes after 10min, sometimes after 15 min and like that. What do you mean by "Maybe follow it into the task preemption (assuming that's till going) and watch it as it returns into each task." How to do that?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

As I say just run the code in a debugger or simulator to the point where it "locks" then break execution to find out where it is stuck (which should then tell you why). If some tasks are still going OK then just keep stepping execution until it gets into the task that is not running.

 

(BTW this is one of the downsides of multi-tasking - it can often be trickier to find out what's wrong when some part of the code stops working).

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

How many tasks do you have? Could you get each task to flash an LED? If each task sets an I/O bit on entry and clears it on exit you could probe with a scope to see what's toggling and what isn't.

 

Or, get each task to write its task number to multiple LEDs. When it freezes the display will indicate the last task to run.

#1 Hardware Problem? https://www.avrfreaks.net/forum/...

#2 Hardware Problem? Read AVR042.

#3 All grounds are not created equal

#4 Have you proved your chip is running at xxMHz?

#5 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand."

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Cliff, there is no preemption happening in the bit of code i wrote. Everything runs to completion.
Sktnandy - see above. Your task can't sit in a loop - when it is called it must do what is necessary and exit within its time slot. If it has to wait for something, you use a state machine, set the state and come back later.

Further to what Brian has suggested, output the task number via the uart. For many years i didn't use (or have) in circuit debug so techniques like flashing leds and uart output are powerful means of identifying what is happening. Even recently i output recovered clock and data signals on i/o bits then used a salae logic analyser to see what was happening.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I found the problem using LED while running it in the simulator. I saw the code is stuck at this I2C routine

  while (!(TWCR & (1 << TWINT)));  // Wait for TWINT flag set in TWCR Register

 

Below is the complete code where it is getting stuck.

 

unsigned char i2c_transmit(unsigned char type) 
{
        switch(type) 
        {
                case I2C_START:    // Send Start Condition
                                                TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
                                                break;
                case I2C_DATA:     // Send Data
                                                TWCR = (1 << TWINT) | (1 << TWEN);
                                                break;
                case I2C_STOP:     // Send Stop Condition
                                                TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
                                                return 0;
        }
  // Wait for TWINT flag set in TWCR Register
  while (!(TWCR & (1 << TWINT)));
  // Return TWI Status Register, mask the prescaler bits (TWPS1,TWPS0)
  return (TWSR & 0xF8);

 

The task which is doing this I2C communication is taking toatl 1ms when it is running fine.I checked it by toggling a bit. 

I dont know why it is stuck at that while() function. Also I am calling the task after every 250ms

Last Edited: Sat. Jan 17, 2015 - 07:13 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You should implement a timeout while waiting for TWINT.

 

Also, examine TWEN when your task hangs.  Is it set?

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

Last Edited: Sat. Jan 17, 2015 - 07:26 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

how much time should I set for TWINT? Have not checked the TWEN flag. what will be the value of TWEN in this case? controller is in master transmitter mode

Last Edited: Sat. Jan 17, 2015 - 08:20 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

checked the TWEN bit. It is set. What could be the reason TWINT bit is not getting set?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

plz help me

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

It could be a number of problems. Do you have the correct pullup resistors? What have you done to investigate the problem? What circumstances lead up to the problem? Use the sound card as an oscilloscope and capture the i2c bus. You'll be able to see where it hangs and hopefully solve the problem yourself.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thnks for the reply. I searched net but it could not find anything to resolve the problem. Pull up resistors are 2k2. As per the code, i2c routine get called after every 250ms. And after a while it gets stop at that line, though the code is going to interrupt after every 10ms. I checked this with a led.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

As i said, it could be a number of things causing the problem. You need to dig for more evidence.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You have no default case in the below so if i2c_transmit(type) is called with type != I2C_START | I2C_DATA | I2C_STOP you won't make any change to TWCR but you will enter the while loop.  Is that what you intended?

 

If not then set a flag (LoopFlag) in cases below, clear LoopFlag in a new default case and add LoopFlag test as condition for While. 

 

David

 

unsigned char i2c_transmit(unsigned char type) 
{
        switch(type) 
        {
                case I2C_START:    // Send Start Condition
                                                TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
                                                break;
                case I2C_DATA:     // Send Data
                                                TWCR = (1 << TWINT) | (1 << TWEN);
                                                break;
                case I2C_STOP:     // Send Stop Condition
                                                TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
                                                return 0;
        }
  // Wait for TWINT flag set in TWCR Register
  while (!(TWCR & (1 << TWINT)));
  // Return TWI Status Register, mask the prescaler bits (TWPS1,TWPS0)
  return (TWSR & 0xF8);

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

*Tickstart's derailment of this thread has been moved to the off-topic forum. Moderator*

Ross McKenzie, Melbourne Australia

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If I understand correctly, there is a heavy dependency on task_timers. In case few task_timers are of 10 msec and their priority is also high, other tasks may not get chance to run. So we have to be careful in selecting  task_timers right? 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

There's a fixed priority with the tasks. Thus a task can 'steal' all the cycles. The general idea is that you don't allow that to happen. Each task does what it needs to do in its 10ms slot and must yield. You can normally do quite a bit of work in that time. Thus the term 'co -operative'.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm a bit late to the party but this page is still very interesting. I was curious if there were problems with changing the section:

void set_task(char tsk)
{
  task_bits |= pgm_read_byte(&bit_mask[tsk]);       /* sets a task bit */
}

to:

void set_task(char tsk)
{
  task_bits |= (1<<tsk);       /* sets a task bit */
}

Or am I missing some information regarding the use of pgm_read_byte(), or that bit shifting is not the best solution in this scenario.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The AVR can only do one shift per cycle (compare this with an ARM that can do n bits in one cycle). My example takes a constant amount of time vs the bit shift taking a variable amount.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Ah k I see. I was also wondering if using a Timer/counter could be an option to measure the cycles a function takes. This is more of an exercise for me to understand parts of my code better.

I was also curious how small the time slices could get. When transmitting/receiving data via UART, I was wondering if your system could handle a lot higher baud rates.

I tried to do some calculations and am wondering on peoples thoughts. These calculations were done with 11bits in mind (1 start, 8 data, 1 parity, 1 stop)

Time between transmissions/receptions in ms = 1/((baud/11)/1000)

  • At 9600 baud it would take 1.146 ms between tx/rx
  • At 250000 baud it would take 0.0440 ms between tx/rx

So the time slices at 9600 baud would be 100us and for the higher baud rate would be 1us.

Does this sound correct?

Cheers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The UART RX has a two-word buffer, plus the incomming shift register, so it can buffer up to three characters.  At 250kbps 8N2, that's 132 us.  At 16 MHz, that's 2112 cycles.

 

Let me guess...  DMX/RDM?  I'd suggest you handle that in an ISR, and buffer an entire DMX frame (or whichever addresses in which you're interested) in SRAM.  Your tasks would then access that buffer.

 

Although you said 8P1, so maybe not DMX...?

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

Last Edited: Wed. Jul 12, 2017 - 12:21 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Ah forgot about the buffer. Would it be sketchy to wait and unload the received words once the buffer and shift register are full? On an interrupt i guess it would not be.

Cheers for making me aware of that. Just went through the datasheet and found what you mentioned, YAY learning!
 

So that is 2112 cycles between each unloading of the 3 bytes? I was trying to work this out to see when having such a high baud was problematic, however every design is situation dependent. And was asking to see if my math was right, which it seems to be.

 

I'm not doing anything fancy yet, as I did had to google DMX/RDM. It was more to learn about this multitasking idea and find the limitations that I need to be aware of.

 

And now I just read the flowchart pic at footer of your post. I feel it is fairly apt considering Im fairly new to MCU's and often dig myself a hole by trying to optimize and write the initial code at the same time.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

3 bytes is the outside limit.  The moment the next start bit arrives, a data overrun condition is flagged with the DORn bit, and the latest incoming word is dropped.

 

There is no way to determine programmatically how many words are waiting in the RX buffer, or how many bits are in the shift register.  The only determination you can make is that the receive buffer contains either:

  • 0 words (UDRE = 1)
  • 1 or more words (UDRE = 0)

 

Some models of AVR have a start frame detection bit, so you could get a warning that there is an incoming word in progress, but that is typically used to wake the device from sleep mode.

 

In practice it is sufficient to service the receive interrupt with every word, so usually the two-deep-buffer-plus-shift-register only ever gets partly full.  The extra depth is useful when your app gets excessively busy on occasion, allowing it to catch up without losing any incoming data, but this behaviour should be profiled to ensure overrun isn't possible, or that it is properly handled if it does.

 

While you can design your app 'to the edge', this must be done with care.

 

Re: flow-chart... Randall Monroe is one of my favourite humans whom I've never met.

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

Hi Kartman

 

Too late on comment..

Where I can see your second tutorial on multitasking??

and if you program, Need to know the following things.

 

-if two task to perform in every 100 mill sec, Then One task at 100th Mill sec and another 110th Mill sec . am I correct?

-What should be the maximum execution time given to a single task to run the all task flawlessly ?? <10mill sec inside a task.

-What should be the minimum interval of a task to repeat all the 8 task.  (task_timers[task])?? Is it >80millsec

 

 

Harbeer

Last Edited: Thu. Jan 31, 2019 - 09:08 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

1) There's only one CPU and there's only so much execution time. If you really have tasks that take a complete 100ms and 110ms to execute then you cannot "tick" at 100ms, you would need to allow more time for their execution.

 

2) Again you may need to "tune" things once you have implemented the task code and determine exactly how long each task needs

 

3) see (2)

 

It's not unusual when using any kind of OS to "tune" things. In preemptive systems this often means changing the task priorities so that everything that needs to run gets its chance. Same is true here. You need to see how long things take and then tune accordingly.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

There is no follow on. Part 1 is the one and only. I didn’t get paid for the first part.
Yes to all the above. The general idea is that the tasks would not always run or run at the same rate. If they run at the same rate you can simply call the 8 tasks in order. The usual application would usually see high priority tasks run on demand and maybe one or two lower priority tasks run at a regular rate.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The important thing to note about Kartman's scheduler is that it is a Run To Completion (RTC) scheduler; it is one type of cooperative scheduler. Cooperative schedulers need every task to be well behaved and to not wait for something to happen (aka block) as that will cause all other tasks to stop running. Every task, when run, does all of its work and then returns to the scheduler. Every task must also complete within its allocated time slot.

#1 Hardware Problem? https://www.avrfreaks.net/forum/...

#2 Hardware Problem? Read AVR042.

#3 All grounds are not created equal

#4 Have you proved your chip is running at xxMHz?

#5 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand."

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Love this; spent the past couple days implementing it into a project I'm working on. Thought I'd share my implementation in case anyone else finds it useful.

 

I have a lot of prints to UART that take longer than my task interval so I have them broken up. I've created a simple mutex to allow one task to complete use of the IO so that it's print isn't mixed up. I'm using the function itself as the owner of the mutex for the purpose of checking the lock.

typedef void (*f_task_ptr_t)(); //typedef for a task function pointer. Using the function address as the 'owner' of the mutex
typedef struct  //mutex struct for managing IO between tasks
{
	uint8_t locked;
	f_task_ptr_t f_task_ptr;
}task_mutex_t;

uint8_t get_mutex(task_mutex_t *mutex, f_task_ptr_t owner);
void release_mutex(task_mutex_t *mutex, f_task_ptr_t owner);

 

As clawson mentioned 11 years ago, I'm using function pointers. I may overly complicate things in the future (something to do with self destructive tendencies) by messing with the priority and/or playing with queues so I wanted to have the option to move things around as needed. But mostly I didn't want to be beholden to keeping tasks named as taskN...now that I have an array of function calls I can just rename the tasks as I please, and then order them in the array. Keep in mind, I'm still constrained by the task_timers array ordering. If I do want to get weird with it and start swapping things around I'll probably just replace the function array with an array of structs that contains both the function pointer and its timer...or with the queue stuff move to a linked list implementation.

 

void reset_task(uint8_t tsk);
void set_task(uint8_t tsk);
void task_dispatch(void);
void task0(f_task_ptr_t self);
void task1(f_task_ptr_t self);
void task2(f_task_ptr_t self);
void task3(f_task_ptr_t self);
void task4(f_task_ptr_t self);
void task5(f_task_ptr_t self);
void task6(f_task_ptr_t self);
void task7(f_task_ptr_t self);

static const __flash f_task_ptr_t tasks[] = {
	(f_task_ptr_t) task0,
	(f_task_ptr_t) task1,
	(f_task_ptr_t) task2,
	(f_task_ptr_t) task3,
	(f_task_ptr_t) task4,
	(f_task_ptr_t) task5,
	(f_task_ptr_t) task6,
	(f_task_ptr_t) task7
};

 

I removed the bit_mask array in favor of doing in-line bit masking off the task number. Not sure if one is more efficient than the other but I'm in favor of not creating additional objects that need to be maintained if I make changes elsewhere (increasing function count for instance). And it seemed excessive to create an object 8x the size of the variable we're checking against.

#define NUM_TASKS 16
uint16_t task_bits = 0;  /* lsb is hi priority task */
uint16_t task_timers[NUM_TASKS]={0,0,0,0,0,0,0,0};          

 

no change here

//start tasks
set_task(0);
set_task(1);
set_task(2);
set_task(3);
set_task(4);
set_task(5);

while(1)
{
    if (ten_milli_seconds)
    {
	ten_milli_seconds = 0;
	task_dispatch();  // well....
    }
}

 

here are the resulting changes from removing the bit mask array

// enable a task for execution
void set_task(uint8_t tsk)
{
    task_bits |= (0x01 << tsk);
}
// disable a task from executing
void reset_task(uint8_t tsk)
{
    task_bits &= ~(0x01 << tsk);
}

 

And here in task_dispatch() you can see the result of changing the function calls to a pointer array. In the original version, the second while loop would run until (task = NUM_TASKS), in this change it's important that doesn't happen or we overrun the function pointer array.

 

And since we're using a function pointer array we can simply call tasks[task]() when we find an active task in bit_mask. We just need to also pass it itself in the same call so it has an identify: tasks[task](tasks[task])

//
//	a task gets dispatched on every execution interval
//
void task_dispatch(void)
{
  /* scan the task bits for an active task and execute it */
  uint8_t task;

/* take care of the task timers. if the value == 0 skip it else decrement it. If it decrements to zero, activate the task associated with it */

    task = 0;
    while (task < NUM_TASKS )
    {
 	if (task_timers[task])
	{
        	task_timers[task]--;            /* dec the timer */
		if (task_timers[task] == 0 ) set_task(task); /* if == 0 activate the task bit */
	}
	task++;
    }

    task = 0; /* start at the most significant task */
    while (task < NUM_TASKS )
    {
	if ((task_bits >> task) & 0x01)
	{
         	//found the task, call it and break out of the loop
		tasks[task](tasks[task]); //the parameter is also the task, that way it knows who it is for the purpose of acessing a mutex
		break;
	}
	task++; /* else try the next one */
    }
}

 

And here's an example of one of my tasks that uses a mutex to manage the UART print. I have an array of structs that holds tmp175_t and here we iterate through it to print out the temperature. Comments in line. I have have another task that does additional UART prints after it finishes going through an array, can show that if you want.

void task3(f_task_ptr_t self)
{
	static char temperature_string[TMP175_STRING_MAX_LENGTH + 1]; //char array to hold UART string. Static so it's only created once
	static uint8_t loopCount = 0; //since we're breaking up the operation, keep track of how many runs through the task we've made.
	uint8_t sensorOffset = loopCount * 4; //Dividing sensor print into 4 sensor groups. Since we're iterating over an array, let's keep track of position.

	//resetting at the top since we're conditionally overwriting it below
	task_timers[3] = 100;		//every 1000ms

	if (debugStreamTMP175) //flag for enabling/disabling the UART stream
	{
		//here we call get_mutex to see if we're able to print. Either it's unlocked, or we're already the owner.
                //we're passing it self so it knows who is knocking.
		if (get_mutex(&fprintf_debug_mutex, self))
		{
			for (uint8_t j = sensorOffset; j < sensorOffset + 4; j++) //print 4 sensors
			{
				//if we have a number of sensors that isn't divisible by 4, then we'd absolutely overrun our array,
				//so do a simple check each time before accessing
				if (j < tmp175_sensor_count)
				{
					if (TMP175_sensors[j].connected)
					{
						//format string takes 3 ms!!!!
						TMP175_format_string(temperature_string, sizeof(temperature_string), TMP175_sensors[j].temperature_unformatted);
						FPRINTF_DEBUG_P("%s:\t%s\n", TMP175_sensors[j].designator, temperature_string);
					}
					else FPRINTF_DEBUG_P("%s:\tNC\n", TMP175_sensors[j].designator);
				}
			}

			//change the task interval so we can finish our print clumped up with everything grouped together.
			//you may not want this!
			if (loopCount >= tmp175_sensor_count) //done
			{
				//release the mutex so that someone else can have it
				//sending self is required so we don't accidentally release it if we don't own it
				release_mutex(&fprintf_debug_mutex, self);
				loopCount = 0; //reset our loop count since it's static
			}
			else //keep going, set set minimum interval so we can bang through the remaining prints.
			{
				loopCount++;
				task_timers[3] = 1;		//every 10ms
			}
		}
		else //we aren't the owner, so let's boost our interval to artificially 'queue' us up for next.
		{
			task_timers[3] = 1;		//every 10ms
		}
	}

	reset_task(3);
}

 

My main goal with most of these changes is to make it scalable, and I wanted to minimize the locations that user error can creep in doing so.

 

For instance every time I added a task, I would copy and paste the old one, but inadvertently forget to change the task number in the reset_task(n) function, or the task_timers[n] array. So as I mentioned above about creating a struct to hold the function and the timer, I may implement that next. Now within each function call rather than call reset_task(n), and potentially have the wrong number as I start to move the task order around, I can call reset_task(function) and pass it the reference of itself...or a struct value...or more likely reset_task() becomes a member of the struct holding the function pointer and task timer. And within each function I simply have to pass self to the mutex functions, not needing to manually send &taskN which could result in the wrong task number.

 

 

Lastly here are the functions I wrote to manage the mutex. 

uint8_t get_mutex(task_mutex_t *mutex, f_task_ptr_t owner)
{
	uint8_t returnValue = false;

	if (mutex->locked && mutex->f_task_ptr != owner) //it's locked and you aren't owner
	{
		 returnValue = false;
	}
	else if (mutex->locked) //it's locked and you are the owner
	{
		returnValue = true;
	}
	else //it's unlocked. making you the owner and locking
	{
		mutex->locked = true;
		mutex->f_task_ptr = owner;
		returnValue = true;
	}
	return returnValue;
}
void release_mutex(task_mutex_t *mutex, f_task_ptr_t owner)
{
	if (mutex->locked && mutex->f_task_ptr == owner) //It's locked and you are the owner so you can clear it
	{
		mutex->locked = false;
		mutex->f_task_ptr = NULL;
	}
}

 

Last Edited: Sun. Oct 31, 2021 - 07:00 AM

Pages