1

First off, I should note that I'm not sure what tags best apply here, and I can't seem to find any that relate to schedulers, etc. Please inform me if they are not the right ones.

I have a simple, portable task scheduler (it only relies on millis(), and runs on different Arduino-supported systems) that is called via a function in loop() and that picks and then runs a function pointer (the "task") from a list of tasks with status, sleep, and priority information.

I would like to have a way to determine and to tell the user how much CPU time is being spent on running tasks vs. being idle.

The problem is that there can be other functions being called from loop(), not just my scheduler. It would thus be a bad idea to track total time vs. time spent in a task. If I did, the CPU use would always read low, since the actual task time cannot use all of the CPU time since the other tasks in loop() would be quietly using some of it.

However, if I specifically track time in the scheduler vs. time spent in task, I run into a new issue. I would use more memory (a few more bytes, but I'm using a lot already...) and more CPU*, since I'd have to keep track of the total so far and also the current runtime for that iteration of the scheduler. That's on top of the similar construct determining the runtime of the task. Mainly, though, the issue is that the loop() function and scheduler run fast enough that the millis() timer can tick slower than the changeover between my scheduler and the loop() function. This causes the timing to also be off, since it would be counted if it ticks in the scheduler even if it was mainly loop()'s fault.

Is there a general methodology for computing the CPU time? It would be difficult to time-slice the scheduler, and that would run slower anyway. I could count the proportion of scheduler calls that run/don't run tasks, but that won't work because tasks take longer than the no-op that happens if a task doesn't run, and it makes no accounting for task length.

*On an AVR system, the 8-bit CPU uses a surprising aggregate time on all those 32-bit operations, between the sleep time calculations (on sleeping tasks only, to be fair) and the elapsed time calculations that fed into my prior CPU use attempt. It wasn't the only cause, but I saw quite a bit of timing slip on one attempt of coding the scheduler because of a similar issue.

2
  • Addressing the issue about mills() being too low resolution, you can also use micros() for a microsecond count, though on some Arduino it has 4µs or 8µs resolution and not true microsecond.
    – romkey
    Aug 23 '20 at 2:33
  • I'm considering that, but the same issue can still apply, just not as much. I'm hoping that there's some sort of well-known or popular approach for getting the CPU load that minimizes this, or at least so that mine won't be any worse. It also doesn't answer what's the better approach here for what's less wrong--do I ignore reading too low from including loop() inadvertently, or do I take the overhead and complexity of tracking only the scheduler's runtime, with reading too low for different reasons? Aug 23 '20 at 3:25
1

The only metric you have is time. The only thing you can do is compare that time over iterations / task calls. The typical approach is to note the time at the start of a task "tick", and note it at the end. Then add the difference to the total for that task.

When you want to then look at usage percentages you can compare those totals to the total time the system has been running.

The total runtime is already done for you, in the form of millis(), so you don't need to worry about that.

Recording each task's total runtime in microseconds is simple enough, but of course will entail an overhead. That is unavoidable no matter what strategy you take. It's kind of quantum: the act of measuring changes the results.

If you treat your "idle" task the same as any other task (which is the normal way of doing things - the idle task is just a task that does nothing) then the sum of all the time counts for all the tasks = (within a certain granularity) the total amount of time your task scheduler has been running. The difference between that and the system runtime (millis()) is the proportion of the time your scheduler has been running compared to other things happening in loop().

Whether you compare to millis() or micros() for the system runtime is up to you. By storing micros() per task you're getting a reasonable resolution for each tasks's runtime (but with a limit to how long before it wraps - but you can handle that in software by carrying over to another lower resolution counter if you want), but you can then discard some of that resolution when doing your calculations if you don't need it at the time.

Estimating runtime in a co-operative multitasking system like this is never good, and often not bothered with because of that. It's always a guestimate with the results being skewed by the measuring, since the measuring is synchronous with the execution of the tasks.

In a proper context-switching multitasking system you would normally operate on the number of "ticks" of the system rather on time. That is, simply count 1 each time a task is switched in by the context switcher. Since each task has a fixed runtime controlled by the granularity of the scheduler it's then trivial to know that task X has been running for X/TOTAL_TICKS % of the time. Any calculations will be themselves done in a task - maybe the idle task, or a system accounting task.

1
  • I think I can get that working. The problem, of course, is that I don't have an idle task; on the event of no task, it simply fails and checks again. It's all in the scheduler itself, not the task. I can't really count zero time, and that skews any measurements I try. But if I can figure out how to make the idle task wait until a task is (almost?) ready, perhaps I can get that working. Or add up the time spent in the scheduler itself, but that's still too small to measure... Aug 24 '20 at 14:07
0

The cheapest (in every sense) way I know to display "idleness" is to set an output pin when you enter the scheduler and clear it again when you dispatch a task (other than your idle task, if there is one). Or reverse this, if you want to display load instead. These can be accomplished with a single instruction each. Low impact on your memory, execution time, assembly time, and wallet.

How precisely do you want to report this?

  • An LED on the output will fade and brighten according to the load. If you need damping (perhaps the load is brief and "peak-y") add an R-C filter to the LED circuit.

  • Connect a DVM, or even better, a mechanical VOM if you can find one, to the output and read load from the meter. A mechanical meter would do some damping for you. A DVM will probably need the filter from the previous suggestion.

  • Loop the filtered output back into an analog input and read the load to a precision of ~ 0.1%! That's way more significance than is justified so round it as you see fit.

1
  • I can't feasibly use that here, but your idea is quite ingenious. I like it! Aug 24 '20 at 14:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.