I've found it's generally best to treat time as a signed integer. The unit isn't really important, I usually just refer to the smallest quantum of time as a tick.
What happens when your timer rolls over from positive to negative? Timer rollover is a controversial subject. I tend to lean toward supporting rollover, but if you can afford the extra overhead, using a datatype that just won't roll over ever sidesteps many potential pitfalls.
I like to decouple system timing from execution time. This can be done by sampling the system time once per program loop and passing it around instead of sampling system time everywhere the current time is needed. This effectively results in time freezing within each execution cycle. This has a bonus effect of making your code easier to unit test!
Of course, this only really makes sense if you are executing everything in the same mainloop. My preferred approach to multitasking is cooperatively.
I generally follow the convention of giving everything that needs to do work periodically a Poll method. The Poll method takes in the current time and somehow returns the next time at which it needs to execute. The promise of the mainloop is to run the Poll again either at or before the specified next time. Since Poll methods are allowed to run more often than necessary, tasks are required to enforce their own scheduling. The mainloop simply runs through everything whenever the time is right, or when an external trigger goes off such as an interrupt or message reception or whatever.
class Task1 { public: bool Poll(int32_t now, int32_t &timeout) { printf("Yay I ran!\n"); timeout = std::min(timeout, 1000); } }; void main() { Task1 task1; int32_t now = millis(); for(;;) { int32_t timeout = INT32_MAX; task1.Poll(now, timeout); // Advance time without slipping. int32_t next = now + timeout; while (next - millis() > 0) {} now = next; } }
Quite often a task just needs to periodically do something. A helper class can make this easy.
class Periodic { public: Periodic(int32_t period) : period_(period) {} bool Init(int32_t start) { next_ = start; } bool IsDue(int32_t now, int32_t timeout) { if (next_ - now <= 0) { // Optionally slip time. // next_ = now + period_; next_ += period_; timeout = std::min(timeout, next_ - now); return true; } timeout = std::min(timeout, next_ - now); return false; } private: const int32_t period_; int32_t next_; }; class Task2 { public: Task2() : cycle_(1000) {} bool Init(int32_t start) { cycle_.Init(start); } bool Poll(int32_t now, int32_t &timeout) { if (cycle_.IsDue(now, timeout)) { printf("Yay I ran at 1hz!\n"); } } private: Periodic cycle_; };
CPU time is a resource just like memory. It's useful to keep track of in an embedded system. There are plenty of ways to do this. RAII could be fun!
struct Bucket { Bucket() { Reset(); } void Reset() { minimum = INT32_MAX; maximum = INT32_MIN; count = 0; } void Add(int32_t delta) { count += delta; minimum = std::min(minimum, delta); maximum = std::max(maximum, delta); } int32_t minimum; int32_t maximum; int32_t count; }; class Measure { public: Measure(Bucket &bucket) : start_(millis()), bucket_(bucket) {} ~Measure() { bucket_.Add(millis() - start_); } private: int32_t start_; Bucket &bucket_; }; void main() { Bucket mainloop_poll; int cycle_count = 0; Task1 task1; Task2 task2; int32_t now = millis(); task2.Init(now); for(;;) { int32_t timeout = INT32_MAX; { Measure(mainloop_poll); task1.Poll(now, timeout); task2.Poll(now, timeout); } if (++cycle_count >= 10) { printf("Average cycle time: %i Min: %i Max %i\n", mainloop_poll.count / cycle_count, mainloop_poll.minimum, mainloop.maximum); mainloop_poll.Reset(); cycle_count = 0; } // Advance time without slipping. int32_t next = now + timeout; while (next - millis() > 0) {} now = next; } }