summaryrefslogtreecommitdiff
path: root/docs/core_runtime.md
blob: 9d81b807be1091261cfd73ab3182237788c60a15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
Chromium OS Embedded Controller runtime
=======================================

Design principles
-----------------

  1. Never do at runtime what you can do at compile time
The goal is saving flash space and computations.
Compile-time configuration until you really need to switch at runtime.

  2. Real-time: guarantee low latency (eg < 20 us)
no interrupt disabling ...
bounded code in interrupt handlers.

  3. Keep it simple: design for the subset of microcontroller we use
targeted at 32-bit single core CPU
for small systems : 4kB to 64kB data RAM, possibly execute-in-place from flash.

Execution contexts
------------------

This is a pre-emptible runtime with static tasks.
It has only 2 possible execution contexts:

- the regular [tasks](#tasks)
- the [interrupt handlers](#interrupts)

The initial startup is an exception as described in the
[dedicated paragraph](#Startup).

### tasks

The tasks are statically defined at compile-time.
They are described for each *board* in the
[board/$board/ec.tasklist](../board/host/ec.tasklist) file.

They also have a static fixed priority implicitly defined at compile-time by
their order in the [ec.tasklist](../board/host/ec.tasklist) file (the top-most
one being the lowest priority aka *task* *1*).
As a consequence, two different tasks cannot have the same priority.

In order to store its context, each task has its own stack whose (*small*) size
is defined at compile-time in the [ec.tasklist](../board/host/ec.tasklist) file.

A task can normally be preempted at any time by either interrupts or higher
priority tasks, see the [preemption section](#scheduling-and-preemption) for
details and the [locking section](#locking-and-atomicity) for the few cases
where you need to avoid it.

### interrupts

The hardware interrupt requests are connected to the interruption handling
*C* routines declared by the `DECLARE_IRQ` macros, through some chip/core
specific mechanisms (e.g. depending whether we have a vectored interrupt
controller, slave interrupt controllers...)

The interrupts can be nested (ie interrupted by a higher priority interrupt).
All the interrupt vectors are assigned a priority as defined in their
`DECLARE_IRQ` macro. The number of available priority level is
architecture-specific (e.g. 4 on Cortex-M0, 8 on Cortex-M3/M4) and several
interrupt handlers can have the same priority. An interrupt handler can only be
interrupted by an handler having a priority **strictly** **greater** than
its own.

In most cases, the exceptions (e.g data/prefetch aborts, software interrupt) can
be seen as interrupts with a priority strictly greater than all IRQ vectors.
So they can interrupt any IRQ handler using the same nesting mechanism.
All fatal exceptions should ultimately lead to a reboot.

### Events

Each task has a *pending* events bitmap[1] implemented as a 32-bit word.
Several events are pre-defined for all tasks, the most significant bits on the
32-bit bitmap are reserved for them : the timer pending event on bit 31
([see the corresponding section](#Timers)), the requested task wake (bit 29),
the event to kick the waiters on a mutex (bit 30), along with a few hardware
specific events.
The 19 least significant bits are available for task-specific meanings.

Those event bits are used in inter-task communication and scheduling mechanism,
other tasks **and** interrupt handlers can atomically set them to request
specific actions from the task. Therefore, the presence of pending events in a
task bitmap has an impact on its scheduling as described in the [scheduling
section](#scheduling-and-preemption).
These requests are done using the `task_set_event()` and `task_wake()`
primitives.

The two typical use-cases are:

- a task sends a message to another task (simply use some common memory
  structures [see explanation](#single-address-space) and want it to process
  it now.
- an hardware IRQ occurred and we need to do some long processing to respond to
  it (e.g. an I2C transaction). The associated interrupt handler cannot do it
  (for latency reason), so it will raise an event to ask a task to do it.

The task code chooses to consume them (or a subset of them) when it's running
through the `task_wait_event()` and `task_wait_event_mask()` primitives.

### Scheduling and preemption

The system has a global bitmap[1] called `tasks_ready` containing one bit
per task and indicating whether or not it is *ready* *to* *run*
(ie want/need to be scheduled).
The task ready bit can only be cleared when it's calling itself one of the
functions explicitly triggering a re-scheduling (e.g. `task_wait_event()`
or `task_set_event()`) **and** it has no pending event.
The task ready bit is set by any task or interrupt handler setting an event
bit for the task (ie `task_set_event()`).

The scheduling is based on (and *only* on) the `tasks_ready` bitmap
(which is derived from all the events bitmap of the tasks as explained above).

Then, the scheduling policy to find which task should run is just finding the
most significant bit set in the tasks_ready bitmap and schedule the corresponding task.

Important note: the re-scheduling happens **only** when we are exiting the interrupt context.
It is done in a non-preemptible context (likely with the highest priority).
Indeed, a re-scheduling is actually needed only when the highest priority task ready has changed.
There are 3 distinct cases where this can happen:

- an interrupt handler sets a new event for a task.
  In this case, `task_set_event` will detect that it is executed in interrupt
  context and record in the `need_resched_or_profiling` variable that it might
  need to re-schedule at interrupt return. When the current interrupt is going
  to return, it will see this bit and decide to take the slow path making a new
  scheduling decision and eventually a context switch instead of the fast path
  returning to the interrupt task.
- a task sets an event on another task.
  The runtime will trigger a software interrupt to force a re-scheduling at its
  exit.
- the running task voluntarily relinguish its current execution rights by
  calling `task_wait_event()` or a similar function.
  This will call the software interrupt similarly to the previous case.

On the re-scheduling path, if the highest-priority ready task is not matching
the currently running one, it will perform a context-switch by saving all the
processor registers on the current task stack, switch the stack pointer to the
newly scheduled task, and restore the registers from the previously saved
context from there.

### hooks and deferred function

The lowest priority task (ie Task 1, aka TASK_ID_HOOKS) is reserved to execute
repetitive actions and future actions deferred in time without blocking the
current task or creating a dedicated task (whose stack memory allocation would
be wasting precious RAM).

The HOOKS task has a list of deferred functions and their next deadline.
Every time it is waken up, it runs through the list and calls the ones whose
deadline is expired. Before going back to sleep, it arms a timer to the closest
deadline.
The deferred functions can be created using the `DECLARED_DEFERRED()` macro.
Similarly the HOOK_SECOND and HOOK_TICK hooks are called periodically by the
HOOKS task loop (the *tick* duration is platform-defined and shorter than
the second).

Note: be specially careful about priority inversions when accessing resources
protected by a mutex (e.g. a shared I2C controller) in a deferred function.
Indeed being the lowest priority task, it might be de-scheduled for long time
and starve higher priority tasks trying to access the resource given there is
no priority boosting implemented for this case.
Also be careful about long delays (> x 100us) in hook or deferred function
handlers, since those will starve other hooks of execution time. It is better
to implement a state machine where you set up a subsequent call to a deferred
function than have a long delay in your handler.

### watchdog

The system is always protected against misbehaving tasks and interrupt handlers
by a hardware watchdog rebooting the CPU when it is not attended.

The watchdog is petted in the HOOKS task, typically by declaring a HOOK_TICK
doing it as regular intervals. Given this is the lowest priority task,
this guarantees that all tasks are getting some run time during the watchdog
period.

Note: that's also why one should not sprinkle its code with `watchdog_reload()`
to paper over long-running routine issues.

To help debugging bad sequences triggering watchdog reboots, most platforms
implement a warning mechanism defined under `CONFIG_WATCHDOG_HELP`.
It's a timer firing at the middle of the watchdog period if it hasn't been
petted by then, and dumping on the console the current state of the execution
mainly to help finding a stuck task or handler. The normal execution is resumed
though after this alert.

### Startup

The startup sequence goes through the following steps:

- the assembly entry routine clears the .bss (uninitialized data),
  copies the initialized data (and optionally the code if we are not executing
  from flash), sets a stack pointer.
- we can jump to the `main()` C routine at this point.
- then we go through the hardware pre-init (before we have all the clocks to
 run the peripherals normal) and init routines, in this rough order:
   memory protection if any, gpios in their default state,
   prepare the interrupt controller, set the clocks, then timers,
   enable interrupts, init the debug UART and the watchdog.
- finally start tasks.

For the tasks startup, initially only the HOOKS task is marked as ready,
so it is the first to start and can call all the HOOK_INIT handlers performing
initializations before actually executing any real task code.
Then all tasks are marked as ready, and the highest priority one is given
the control.

During all the startup sequence until the control is given the first task,
we are using a speciak stack called 'system stack' which will be later re-used
as the interrupts and exception stack.

To prepare the first context switch, the code in `task_pre_init()` is stuffing
all the tasks stacks with a *fake* saved context whose program counter is
containing the task start address and the stack pointer is pointing to its
reserved stack space.

### locking and atomicity

The two main concurrency primitives are lightweight atomic variables and
heavier mutexes.

The atomic variables are 32-bit integers (which can usually be loaded/stored
atomically on the architecture we are supporting). The `atomic.h` headers
include primitives to do atomically various bit and arithmetic operations
using either load-linked/load-exclusive, store-conditional/store-exclusive
or simple depending what is available.

The mutexes are actually statically allocated binary semaphores.
In case of contention, they will make the waiting task sleep
(removing its ready bit) and use the [event mechanism](#Events) to wake-up
the other waiters on unlocking.

Note: the mutexes are NOT triggering any priority boosting to avoid the
priority inversion phenomenon.

Given the runtime is running on single core CPU, spinlocks would be equivalent
to masking interrupts with `interrupt_disable()` spinlocks, but it's
strongly discouraged to avoid harming the real-time characterics of the runtime.

Time
----

### time keeping

In the runtime, the time is accounted everywhere using a
**64-bit** **microsecond** count since the microcontroller **cold** **boot**.

Note: The runtime has no notion of wall-time/date, even though a few platform have
an RTC inside the microcontroller.

These microsecond timestamps are implemented in the code using the `timestamp_t`
type and the current timestamp is returned by the `get_time()` function.

The time-keeping is preferably implemented using a 32-bit hardware
free running counter at 1Mhz plus a 32-bit word in memory keeping track of
the high word of the 64-bit absolute time. This word is incremented by the
32-bit timer rollback interrupt.

Note: as a consequence of this implementation, when the 64-bit timestamp is read
in interrupt context in an handler having a higher priority than the timer IRQ
(which is somewhat rare), the high 32-bit word might be incoherent (off by one).

### timer event

The runtime offers *one* (and only one) timer per task.
All the task timers are multiplexed on a single hardware timer.
(can be just a *match* *interrupt* on the free running counter mentioned in the
[previous paragraph](#time-keeping))
Every time a timer is armed or expired, the runtime finds the task timer having
the closest deadline and programs it in the hardware to get an interrupt.
At the same time, it sets the TASK_EVENT_TIMER event in all tasks whose timer
deadline has expired.
The next deadline is computed in interrupt context.

Note: given each task has a **single** timer which is also used to wake-up the
task when `task_wait_event()` is called with a timeout, one needs to be careful
when using directly the `timer_arm()` function because there is an eventuality
that this timer is still running on the next `task_wait_event()` call, the call
will fail due to the lack of available timer.

Memory
------

### Single address space

There is no memory isolation between tasks (ie they all live in the same address
space). Some architectures implement memory protection mechanism albeit only to
differentiate executable area (eg `.code`) from writable area (eg `.bss` or
`.data`) as there is a **single** **privilege** level for all execution contexts.

As all the memory is implicitely shared between the task, the inter-task
communication can be done by simply writing the data structures in memory
and using events to wake the other task (given we properly thought the concurrent
accesses on thoses structures).

### heap

The data structure should be statically allocated at compile time.

Note: there is no dynamic allocator available (e.g. `malloc()`), not due to
impossibility to create one but to avoid the negative side effects of
having one: ie poor/unpredictable real-time behavior and possible leaks
leading to a long-tail of failures.

- TODO: talk about shared memory
- TODO: where/how we store *panic* *memory* and *sysjump* *parameters*.

### stacks

Each task has its own stack, in addition there is a system stack used for
startup and interrupts/exceptions.

Note 1: Each task stack is relatively small (e.g. 512 bytes), so one needs to
be careful about stack usage when implementing features.

Note 2: At the same time, the total size of RAM used by stacks is a big chunk
of the total RAM consumption, so their sizes need to be carefully tuned.
(please refer to the [debugging paragraph](#debugging) for additional input on
this topic.

## Firmware code organization and multiple copies

- TODO: Details the classical RO / RW partitions and how we sysjump.

power management
----------------

- TODO: talk about the idle task + WFI (note: interrupts are disabled!)
- TODO: more about low power idle and the sleep-disable bitmap
- TODO: adjusting the microsecond timer at wake-up

debugging
---------

- TODO: our main tool: serial console ...
(but non-blocking / discard overflow, cflush DO/DONT)
- TODO: else JTAG stop and go: careful with watchdog and timer
- TODO: panics and software panics
- TODO: stack size tuning and canarying


- TODO: Address the rest of the comments from https://crrev.com/c/445941

[1]: bitmap: array of bits.