[38;5;244m┌─[0m [38;5;40mroot@joaocosta:~#[0m [38;5;245mcat posts/9.md[0m [38;5;244m────────────────────────────────────────────[0m

[38;5;40m#[0m [1muKernel DevLog #1[0m
[38;5;245mpublished 2022-05-02 · 7 min read · https://joaocosta.dev/blog/9[0m

I'm currently enrolled in a class about embedded and real-time systems. For this
class's final project, I'm developing a real-time kernel for [38;5;40mArduino UNO[0m
[38;5;244m(https://www.arduino.cc/en/Main/arduinoBoardUno)[0m. I'll try to document the
development in a series of posts. This is part #1.

Today (as of writing) was the exam's day: the [3mfinal[0m exam was in the middle of
the semester. As such, the rest of lectures are reserved for the development of
the project. My team (me and 2 friends) decided to develop a micro-kernel
(uKernel) for the Arduino UNO. The Arduino wasn't chosen for any reason in
particular (besides being available at the school's lab).

[38;5;38m##[0m [1mThe basics[0m

Around 2 weeks ago, we had a guided [3mworkshop[0m to develop a small Kernel for the
Arduino (in C). It was a [1mtick-based preemptive[0m kernel supporting a fixed
task-set of periodic tasks with [1mfixed priorities[0m. This kernel had both a
scheduler and a dispatcher, so we had what's called [1mon-line scheduling[0m. The
tasks' [1mdeadlines[0m are equal to their [1mperiods[0m.

[38;5;135m###[0m [1mExplaining concepts[0m

[38;5;40m→[0m [1mTask[0m - A set of instructions that has to be run according to some time
  constraints;
[38;5;40m→[0m [1mPeriodic task[0m - A task that takes [3mC[0m time units to complete and needs to be run
  every [3mT[0m time units;
- Note that this doesn't mean that a task (C=2ms, T=10ms) executes for 2ms and
then executes again, that 10ms later. It means that every 10ms, 2ms will be
taken to execute the task.

[38;5;40m→[0m [1mPeriod ([3mT[0m)[0m - The time between task activations.
[38;5;40m→[0m [1mFixed priorities[0m - A static attribute assigned to each task:
- Higher numbers mean higher priority; - Used by the [1mscheduler[0m to choose which
task to run at a given instant.

[38;5;40m→[0m [1mTick-based[0m - A [1mtick-based[0m kernel (like the name implies) uses timer interrupts
  to schedule and dispatch tasks;
[38;5;40m→[0m [1mPreemptive[0m - When a kernel supports preemption, it means that tasks can be
  paused

[38;5;244m────────────────────────────────────────────────────────────────────────────────[0m

[38;5;40m→[0m [38;5;245m[image][0m Preemption example diagram
[38;5;244mhttps://joaocosta.dev/_resources/d07b520cb26663312dea7f2ff8f1aaae.png[0m

[38;5;135m###[0m [1mSource code[0m

[38;5;244m┄─────────────────────────────────────────────────────────────────────────── c [0m
[38;5;245m│[0m #define NT 20 // the max number of tasks in the system
[38;5;245m│[0m 
[38;5;245m│[0m typedef struct
[38;5;245m│[0m {
[38;5;245m│[0m     unsigned int period;
[38;5;245m│[0m     unsigned int delay;
[38;5;245m│[0m     void (*func)(void);
[38;5;245m│[0m     unsigned int exec;
[38;5;245m│[0m } Task;
[38;5;245m│[0m 
[38;5;245m│[0m Task tasks[NT];
[38;5;245m│[0m unsigned int curr_task = NT + 1;
[38;5;245m│[0m 
[38;5;245m│[0m /*
[38;5;245m│[0m  * Registers a task in the system. The delay controls the initial offset of the task.
[38;5;245m│[0m  * A task with 0 period is only run once (one-shot task).
[38;5;245m│[0m  */
[38;5;245m│[0m int Sched_Add(unsigned int period, unsigned int delay, void (*func)(void), unsigned prio) {
[38;5;245m│[0m     if (!tasks[prio].func) {
[38;5;245m│[0m             tasks[prio] = {
[38;5;245m│[0m                 period,
[38;5;245m│[0m                 delay,
[38;5;245m│[0m                 func,
[38;5;245m│[0m                 (delay == 0)
[38;5;245m│[0m             };
[38;5;245m│[0m             return prio;
[38;5;245m│[0m     }
[38;5;245m│[0m     return -1;
[38;5;245m│[0m }
[38;5;245m│[0m 
[38;5;245m│[0m /* Ticks all tasks: updates the current time till ativation, and sets tasks ready */
[38;5;245m│[0m void Sched_Schedule() {
[38;5;245m│[0m     for (int i = 0; i < NT; ++i ) {
[38;5;245m│[0m         if (!tasks[i].func || tasks[i].exec)
[38;5;245m│[0m             continue;
[38;5;245m│[0m 
[38;5;245m│[0m         if (tasks[i].delay == 0) {
[38;5;245m│[0m             tasks[i].exec = 1;
[38;5;245m│[0m             tasks[i].delay = tasks[i].period + 1;
[38;5;245m│[0m         }
[38;5;245m│[0m         --tasks[i].delay;
[38;5;245m│[0m     }
[38;5;245m│[0m }
[38;5;245m│[0m 
[38;5;245m│[0m /* Runs the highest priority ready task */
[38;5;245m│[0m void Sched_Dispatch() {
[38;5;245m│[0m     int prev_task = curr_task;
[38;5;245m│[0m 
[38;5;245m│[0m     for (int i = 0; i < prev_task; ++i) {
[38;5;245m│[0m         if (tasks[i].func && tasks[i].exec) {
[38;5;245m│[0m             tasks[i].exec = 0;
[38;5;245m│[0m 
[38;5;245m│[0m             // run task
[38;5;245m│[0m             curr_task = i;
[38;5;245m│[0m             interrupts();
[38;5;245m│[0m             tasks[i].func();
[38;5;245m│[0m             noInterrupts();
[38;5;245m│[0m             curr_task = prev_task;
[38;5;245m│[0m 
[38;5;245m│[0m             // delete one-shot tasks (tasks that only run once)
[38;5;245m│[0m             if (tasks[i].period == 0) {
[38;5;245m│[0m                 tasks[i].func = 0;
[38;5;245m│[0m             }
[38;5;245m│[0m         }
[38;5;245m│[0m     }
[38;5;245m│[0m }
[38;5;245m│[0m 
[38;5;245m│[0m // Timer1 interrupt handler
[38;5;245m│[0m ISR(TIMER1_COMPA_vect) {
[38;5;245m│[0m     Sched_Schedule();
[38;5;245m│[0m     Sched_Dispatch();
[38;5;245m│[0m }
[38;5;245m│[0m 
[38;5;245m│[0m void setup(){
[38;5;245m│[0m   Sched_Add(10, 5, FuncX, 0);
[38;5;245m│[0m   Sched_Add(4,  0, FuncY, 1);
[38;5;245m│[0m   Sched_Add(1,  0, FuncZ, 2);
[38;5;245m│[0m 
[38;5;245m│[0m   // Setup timer interrupts on timer1
[38;5;245m│[0m }
[38;5;245m│[0m 
[38;5;245m│[0m void loop(){
[38;5;245m│[0m   // do nothing
[38;5;245m│[0m }
[38;5;244m┄──────────────────────────────────────────────────────────────────────────────[0m

In the code above, I've omitted some details like the implementation of the
tasks function ([38;5;38mFuncX[0m, [38;5;38mFuncY[0m, [38;5;38mFuncZ[0m just toggle their respective LED) and how to
set up timer interrupts, for simplicity’s sake.

[38;5;135m###[0m [1mCode analysis[0m

Given how scary "implementing a micro-kernel" sounds, the code above is fairly
simple. In my opinion, the most elegant part is how preemption is handled:

[38;5;40m→[0m When handling an interrupt, interrupts are automatically disabled (and
  re-enabled on return from the interrupt handler);
[38;5;40m→[0m Before running the task's code, we re-enable interrupts (and disable them
  afterwards);
[38;5;40m→[0m If the task is long enough for another timer interrupt to happen, the [3mISR[0m can
  start executing another task;
[38;5;40m→[0m When a task returns, and the [3mISR[0m exits, the previous task's execution will be
  resumed.

Another cool property is how [1mtask overruns[0m are handled. A [1mtask overrun[0m is when a
task runs for longer than the maximum time (worst-case) it said it would run
for. In the case of this micro-kernel, the task's [1mdeadlines are equal to the
periods[0m ([38;5;38mDi = Ti[0m). If a task runs for longer than its period, it can't prempt
itself. The cool thing about this is that the effect of these [1moverruns[0m is
[1mdeterministic[0m: they only affect tasks of lower priority (they get delayed).

But there are some problems with this implementation:

[38;5;40m→[0m The kernel uses fixed priorities, but they aren't sorted in any particular
  order. This makes [1mschedulability testing[0m unfeasible;
[38;5;40m→[0m We can't configure a task to have [1mdeadlines[0m smaller than its [1mperiod[0m ([38;5;38mDi < Ti[0m);
[38;5;40m→[0m The tasks are assumed independent, so there aren't [1mpriority inversion[0m
  ([1mblocking[0m) problems with the kernel;
[38;5;40m→[0m For the same reason, [1mtask synchronization (semaphores)[0m/[1mmutual exclusion
  (mutexes)[0m mechanisms aren't available.

[38;5;38m##[0m [1mImproving the kernel[0m

[38;5;135m###[0m [1mTask schedulability[0m

When we have a real-time system, we're interested in determining if the task set
can be scheduled in a way that no deadlines are missed before deploying the
system.

In the case of this kernel (considering fixed priorities and [38;5;38mDi = Ti[0m), there are
2 alternative methods for [3mpriority assignment[0m:

[38;5;40m→[0m [1mRate Monotonic (RM)[0m - The priorities are inversely proportional to the [1mperiod[0m.
  This means that the lower the [1mperiod[0m, the higher the [1mpriority[0m. Only works for
  [38;5;38mDi = Ti[0m;
[38;5;40m→[0m [1mDeadline Monotonic (DM)[0m - The priorities are inversely proportional to the
  [1mdeadline[0m. This means that the lower the [1mdeadline[0m, the higher the [1mpriority[0m.
  Works for [38;5;38mDi <= Ti[0m.

Let's the following task set with RM priorities:

[38;5;245m| priority | T   | C   |[0m
[38;5;245m| 1        | 2   | 0.5 |[0m
[38;5;245m| 2        | 3   | 0.5 |[0m
[38;5;245m| 3        | 6   | 2   |[0m

The lower the priority, the higher the [1mperiod (T)[0m. [1mC[0m is the amount of CPU time
the task needs to complete execution (worst-case).

To verify the schedulability of this task-set, we perform a [1mCPU utilization[0m test
using [3mLiu & Layland's[0m [1mLeast Upper Bound (LUB)[0m:

[38;5;40m→[0m [1mCPU utilization - U(n)[0m - Is the sum of all tasks utilization: [38;5;38mCi/Ti[0m;
[38;5;40m→[0m [1mLUB[0m - [38;5;38mn * (2^(1/n) - 1)[0m, where [38;5;38mn[0m is the number of tasks in the system.
[38;5;40m→[0m [38;5;38mU(n) > 1[0m => non schedulable set (overload);
[38;5;40m→[0m [38;5;38mU(n) <= LUB[0m => schedulable set;
[38;5;40m→[0m [38;5;38m1 >= U(n) >= LUB[0m => undetermined case. The test doesn't always work/grantee if
  the system is schedulable/unschedulable.

The image below shows this test being applied to the task-set above.

[38;5;40m→[0m [38;5;245m[image][0m LUB test
[38;5;244mhttps://joaocosta.dev/_resources/141d8b59089e7d099a33d585a9557aaa.png[0m

As the image displays, the [1mtask-set is schedulable[0m.

[38;5;208m####[0m [1mHarmonic periods – special case[0m

If the periods of all tasks are harmonic (all multiples of each other), we
compare the CPU utilization to 100% (instead of the LUB): [38;5;38mU(n) <= 1[0m.

It should be noted that, if the [1mperiods[0m aren't harmonic, we can't grantee 100%
CPU utilization with fixed priority systems.

[38;5;40m→[0m [38;5;245m[image][0m Example of harmonic periods
[38;5;244mhttps://joaocosta.dev/_resources/c9d5597207431601059a0d289b2881ea.png[0m

In the image above, we have 2 systems with 2 tasks each:

[38;5;40m→[0m In the first system, the periods are harmonic. The tasks use 100% of the CPU,
  and the task-set is schedulable;
[38;5;40m→[0m In the second system, the periods aren't harmonic. In fact, there is less
  "load" in the system as the second task has more [3mslack[0m (doesn't run as often).
  Still, we aren't using 100% of the CPU and if we reduce the period of any of
  the tasks for any amount (make them more frequent), the task-set won't be
  schedulable.

To solve the problem above, we can use [1mdynamic priority[0m systems: a task's
priority is only known/calculated at run-time by the scheduler.

[38;5;135m###[0m [1mBlocking situations[0m

[1mBlocking[0m happens there are dependencies/sequences between tasks. For example, 2
tasks share a [1mmutex[0m for a given critical region. [3mTask 1[0m (higher priority), tries
to preempt [3mtask 2[0m, but can't execute, because the [1mmutex[0m is locked in _task 2_'s
critical region. In this kind of situation, [3mtask 1[0m can run for a while before
reaching the critical region, but when it reaches the region, it will have to
return control to [3mtask 2[0m before proceeding.

When this happens, we're going back on our chain of tasks. This means that when
the control returns to the lower priority task, it can override that higher
priority's stack data (for example, by calling a function).

[38;5;40m→[0m [38;5;245m[image][0m Block stack corruption example
[38;5;244mhttps://joaocosta.dev/_resources/aac46ddffd2050da1d801902bb2f210b.png[0m

In the image above, we see how a function call from the lower priority task
(after blocking the higher priority one) can corrupt the stack.

To deal with this, we'll implement mechanisms to manage the access to shared
resources, and their protection mechanisms. Most of these solutions require us
to implement multiple stacks: one for each task.

[38;5;135m###[0m [1mShared resource access[0m

There are several solutions available to us here depending on which techniques
of shared resource access control we want to implement. These techniques divide
themselves into 2 main groups:

[38;5;40m→[0m Global - Interrupt disabling and/or preemption disabling. These are only
  feasible for extremely small critical regions as they block the whole system.
  We won't really deal with this in our implementation.
[38;5;40m→[0m Local - [1mMutexes[0m and [1msemaphores[0m.

The local methods are more efficient, but they can introduce some other
problems, namely: undetermined blocking, chained blocking, and deadlocks. The
protection mechanisms intend to mitigate one or more of these. We'll explore
these in the future (when we're implementing them).

[38;5;38m##[0m [1mConclusion[0m

I tried to explain most of what I find the [3mfundamental[0m concepts of Real-time
systems, so next posts can be easier to follow. I hope it wasn't too much.

In the next chapter, we'll explore how we can implement multiple stacks: one for
each task. Spoiler: there's assembly involved. We'll also start using C++ where
we can, so we can clean up the code by using classes.

Stay safe :P

[38;5;244m└─[0m [38;5;40m→[0m view online: [38;5;40mhttps://joaocosta.dev/blog/9[0m
