About events
All events (external stimuli) that are received by a job are delivered using the very same principle:
The job call the sys_wait_for_event() syscall, in a blocking or non-blocking mode
If an event that match the event type(s) requested, the event is pushed back to the job’s svc_exchange area, using a unified header, making the event being encoded as a datagram, using a dedicated event header
The job parse and react the event frame received
The only exception to this standard behavior are Shared Memories, built for performance.
The unified event header is defined as the following:
wait_for_event unified header format{ event_type: u8, event_length: u8, event_magic: u16, event_source: u32, }
The event header format respects the following fields specification:
event_type: the event type identifier, as defined in the EventType enumerate
event length: event data length in bytes. The header length is not considered in this field
event_magic: uniquely defined magic that allows to verify that such a header is an event header
event_source: source of the event, being the kernel (identified by 0) or another job, identified by its task handle
Todo
The event header magic is, by now, hardcoded to 0x4242. The magic value aims to be project-specified instead
Event data is then stored just after the event header.
Note
Event header and data is kept is the native endianness, being little or big depending on the current platform endianness. There is no endianness conversion when receiving events
About DMA streams
DMA streams are objects that are defined at compile-time, so that they can be manipulated at run-time by the owning task. A stream is a static definition of a DMA channel configuration that needs to be instantiate at runtime when needed.
Such an object, like other Sentry objects (devices, shared-memory):
have a statically defined owner
is associated to a capability (CAP_DEV_DMA)
has a unique label that identify the stream
In DMA stream definition, the ownership is defined at channel level to ensure that there is no way to share a given DMA channel between tasks, to avoid any potential covert channel
In the same way, DMA streams can’t read neither write anywhere in memory. As Sentry has the notion of reserved-memory blocks statically owned by tasks, memory targeting DMA streams can only read (as source) or write (as target) shared memories that hold the dma-pool attribute. As shared-memories are not shared by default (see above), it is possible to control source and destination of a DMA streams in term of ownership.
Sentry supports usual DMA streams types:
Memory to memory: DMA copy between shared memories
Device to memory: DMA copy from a device rx FIFO and a shared memory
Memory to device: DMA copy from a shared memory toward a device tx FIFO
Device to Device: DMA copy between chained devices
DMA stream definition must comply with the following specification:
compatible = "dma-streams"”: (required) define the current block as a dma-stream
channel: (reqquired) target channel identifier, as defined in any activated GPDMA controller
streamid: when interacting with a device, stream identifier as defined in the GPDMA datasheet that is defined for such a device interaction. This value is not validated by Sentry at runtime as it is a SoC-specific value
prio: (required): DMA stream priority, as defined in the currently used dt-bindings header
source: (required) Sentry object source, being an existing shared memory or a device, using DTS phandle reference
dest: (required) Sentry object destination, being an existing shared memory or a device using DTS phandle reference
length: (required) amount of bytes to transfer
circular: when the source or the destination requires a circular write, set circular flag to 1 using <source dest> booleans
sentry,label: (required) unique strem identifier to be used when requiring the DMA handle value
Warning
DMA API do not verify target or source memory ownership of a DMA stream for the sake of kernel implementation simplicity. As streams are build-time defined, reviewing the device-tree is considered instead of enabling run-time complex checks
Multiple DMA streams can target the same DMA channel, while the DMA stream owner is the same for all streams. The DMA owner stream owner is then responsible for consecutively assign, start, stop and unassign streams.
1 dma-streams {
2 // memory-to-memory DMA stream
3 stream1 {
4 compatible = "dma-stream";
5 channel = <&gpdma1_1>;
6 prio = <STM32_DMA_PRIORITY_HIGH>;
7 source = <&shm__1>;
8 dest = <&shm__2>;
9 length = <0x100>;
10 // no circular, linear for both source and dest
11 sentry,label = <0x2>; // task-level unique DMA identifier
12 };
13
14 stream2 {
15 compatible = "dma-stream";
16 channel = <&gpdma1_1>;
17 streamid = <112>; // channel stream (af) identifier
18 prio = <STM32_DMA_PRIORITY_MEDIUM>;
19 source = <&usart1>;
20 dest = <&shm_autotest_1>;
21 length = <42>;
22 circular = <1 0>; // circular source, linear dest
23 sentry,label = <0x1>; // task-level unique DMA identifier
24 };
25 };
26
27 [...]
28 // GPDMA 1 active channels
29 &gpdma1 {
30 status = "okay";
31 // About channels that are used
32 gpdma1_1: dma-channel@1 {
33 status = "okay";
34 sentry,owner = <0xbabe>;
35 };
36 };
Note
A DMA stream is declared in the root (denoted /) section of the device tree
When receiving a DMA stream event, the DMA event is encoded as a u32. DMA event length is always 4.
DMA event are defined in the dma.h header, and respect the following potential values:
1GPDMA_STATE_TRANSMISSION_FAILURE /**< DMA transmission failure */
2GPDMA_STATE_CONFIGURATION_FAILURE /**< DMA channel configuration failure */
3GPDMA_STATE_OVERRUN /**< DMA transmission overrun */
4GPDMA_STATE_TRANSFER_COMPLETE /**< DMA transfer complete for this channel */
5GPDMA_STATE_HALF_TRANSFER /**< DMA transfer half-complete for this channel */
Todo
properly separate state (returned by get_info/get_status) from events
Todo
DMA unassign still need to be implemented.
About signals
Signals is the easiest communiation channel for a task. Signals are asynchronous events a job may receive in various cases:
Another job has emitted a signal that target the current job
A system-related event has risen and the kernel has emitted the signal to the current job
In order to differenciate both events type, the source field of the event header is used:
When the signal is emitted by another job, the corresponding task handle is set as source
When the event is emitted by the kernel, the source field of the event header is set to 0
Sentry supports the following signals that can be canonically used by any task:
SIGNAL_ABORT = 1
SIGNAL_ALARM,
SIGNAL_BUS,
SIGNAL_CONT,
SIGNAL_ILL,
SIGNAL_IO,
SIGNAL_PIPE,
SIGNAL_POLL,
SIGNAL_TERM,
SIGNAL_TRAP,
SIGNAL_USR1,
SIGNAL_USR2,
These signals are mapped on a subset of the POSIX PSE51 signals definition, as they well defined various events that can be used as a control plane implementation between tasks that interact as a system-level automaton. USR1 and USR2 are also defined for other cases that are not defined by others.
Kernel-related signal-encoded events that may also rise are the following:
SIGNAL_PIPE: an IPC targetting another job is broken, as, for any reason, the other jobs terminates without reading the IPC content. The job is then awoken from its blocking send_ipc() call with a STATUS_INTR return code.
SIGNAL_ALARM: if the job as requested an alarm scheduling (see sys_alarm() syscall), this signal is emitted when alarm timeout is reached.
SIGNAL_CONT: if the system has just leaving a low power mode, the kernel emit such a signal to all running jobs.
When receiving a signal event, the signal is encoded as a u8. signal event length is always 1.
About Interrupts
Interrupts events rise when a given task own a driver that is associated to a device for which at least one interrupt has been defined in the device tree.
While the current SoC dtsi file, delivered in the camelot-devicetree repository is clean for the device, there is no need to add interrupt related information, as interrupt assignations is already defined. As a xonsequence, only the device activation (using the status = “okay”; standard attribute) is enough.
Note
It is possible to validate that the device is properly specified in the generated dts file using, for example, dts_dump
$ dts_dump subprojects/devicetree/nucleo_u5a5_autotest.dts.pp timers6
timers6: timers@40001000 {
compatible = "st,stm32-timers";
reg = < 0x40001000 0x400 >;
clocks = < &rcc 0x9c 0x10 >;
resets = < &rctl 0xe84 >;
interrupts = < 0x31 0x0 >;
interrupt-names = "global";
status = "okay";
sentry,owner = < 0xbabe >;
sentry,label = < 0x1f01 >;
sentry,counter = < 0xffff >;
sentry,prescaler = < 0x262 >;
pwm {
compatible = "st,stm32-pwm";
status = "disabled";
#pwm-cells = < 0x3 >;
};
};
Using such a configuration, the declared interrupts are assigned to the owning job. Unmasking an interrupt line is not an automated behavior and is a voluntary action using the sys_irq_enable UAPI.
On the other side, when an IRQ that target a user task rise, the kernel voluntary mask the IRQ line as the effective device IRQ handler is executed in the task context, and as such behave as a threaded IRQ. The job is then responsible for:
execute the effective ISR routine (such as clearning the device status flag(s))
re-enable the IRQ line no that the device event has been properly acknowledged
A typical user-space IRQ handling is described below:
When an IRQ rise, the kernel push the IRQ information at task input queue level and schedule the job. Although, the job is not elected (i.e. there is no scheduling violation in the case of IRQ events). As a consequence, the associated latency depend on the job priority and quantum. This, depending on the configuration, generates latency in the election of the IRQ handling thread.
There are times when multiple IRQn are waiting in the current task input queue. In that case, the job can received, in a single wait_for_event call, multiples IRQs.
This allows the job to decide with which priority/hierarchy all IRQn should be treated.
Todo
By now, sentry only push one interrupt at a time
When receiving an IRQ event, the IRQn value is encoded as a u32. IRQ event length is always a multiple of 4, depending on the number of waiting interrupt(s) that have been pushed to the user