For implementing tasks for your application code, you need to pick the proper execution primitive (pre-emptible thread, cooperative thread, work queue, etc.) and set its priority correctly to not block other tasks that are running on the CPU while you also meet the time requirements of the task.
One of the main goals of this lesson is to learn how to schedule application tasks using the right execution primitive with the right priority level. But before we can dive into the topic of choosing the right execution method to run a given task, we need to consider the following questions:
1. Early Boot Sequence (C Code Preparation phase)
The primary function of the early boot sequence is to transition the system from its reset state to a state where it becomes capable of executing C code, thereby initiating the kernel initialization sequence. This stage is a pretty standard phase in embedded devices; as an application developer, it is not of much interest.
2. Kernel Initialization
This stage presents a process of initializing the state for all enabled static devices. Some devices are enabled out-of-the-box by the RTOS, while others are enabled by your application configuration file (prj.conf
) and your board configuration files as we learned in the nRF Connect SDK Fundamentals course. These latter devices encompass device driver and system driver objects that are defined using Zephyr APIs in a static manner.
The initialization order is controlled by assigning them to specific run levels (for example, PRE_KERNEL_1
, PRE_KERNEL_2
), and their initialization sequence within the same run level is further defined using priority levels. Keep in mind that at this stage, the scheduler and the Kernel services are not yet available, so these initialization functions do not rely on any Kernel services. We will dive into driver initialization in Lesson 7.
2.1. What gets initialized in PRE_KERNEL_1
by default in all nRF Connect SDK applications
2.2. What gets initialized in PRE_KERNEL_2
by default in all nRF Connect SDK applications
k_sleep()
and the kernel timer API.The list of devices does not include all devices that get initialized in your application. The list only provides the minimal core devices needed by the RTOS. The devices and subsystems initialized will depend on your application configuration and your board configuration file. In the next lesson, we will dive more into how to find out all configured devices and subsystems.
3. Multithreading Preparation
This is where the multithreading features get initialized, including the scheduler. The RTOS will also create two threads (System threads): The RTOS main thread, and the idle thread that is responsible for calling the power management system of the SiP and SoC if no other threads are ready.
During this phase, the POST_KERNEL
services are initiated, if any exist. Once the POST_KERNEL
services are initiated, the Zephyr boot banner is printed:
*** Booting nRF Connect SDK v2.x.x ***
After that, the APPLICATION
level services are initialized, if any exist. Then, all application-defined static threads (using K_THREAD_DEFINE()
) are initiated.
3.1. What gets initialized in POST_KERNEL
by default in all nRF Connect SDK applications
This is where many libraries, RTOS subsystems, and services get initialized. These libraries require kernel services during configuration so this is why they are initiated in the POST_KERNEL
level, where the kernel services are available.
By default, the RTOS does not initialize anything here. However, there are many libraries, RTOS subsystems, and services that get initialized here if they are enabled. For instance, if logging (CONFIG_LOG
)is enabled with deferred mode, this is where the logger module gets initiated, and the logger-dedicated thread (for deferred mode) gets created. Also, if Bluetooth Low Energy is used (CONFIG_BT
), this is the location where the Bluetooth stack gets initialized, and the RX and TX threads get created. Similarly, if the system work queue is used, this is the location where the system work queue thread is initialized.
3.2. What gets initialized in APPLICATION
by default in all nRF Connect SDK applications
By default, some libraries get initiated here if they are enabled. For instance, if you are developing on the nRF91 Series and you are using the AT Monitor Library (AT_MONITOR
), this is the location where this library gets initialized.
The RTOS main thread is the currently active thread. After it’s done with all initializations, it will call your main()
function at the end, if it exists. If no user-defined main()
exists, the RTOS main thread will terminate, and the scheduler will pick the next ready thread for execution. This could be a user-defined thread, a subsystem thread, or the idle thread if there are no ready threads. What decides which thread will execute is the type and priority of the threads. This is something we will cover in the next topics.
For multi-core hardware such as the nRF5340 SoC, other peripherals, such as the mailbox (mbox), will be initialized on bootup by the SDK. When Trusted Firmware-M is used, an entropy source such as the psa-rng peripheral will be initialized as well.
After boot-up, there will be several threads and interrupts set up. In the next section, we will compare the context where interrupts run in, called interrupt context, and the context where threads run in, called thread context.
Interrupt and thread contexts refer to different execution contexts with distinct characteristics and intended usages. Let’s explore each context and the allowed and not allowed operations within them:
Execution Context: Thread context refers to the normal execution environment where application and system threads run.
Triggering Event: Threads are created by the application or the RTOS and scheduled by the scheduler using defined rules (type and priority).
Preemption: Thread context can be preempted by an interrupt or higher-priority threads. You can find more details on this in the Scheduler in-depth topic.
Duration: Threads can execute longer and perform more complex operations than interrupt context.
Allowed Operations:
Not Allowed Operations:
Execution Context: Interrupt context refers to the execution environment when an interrupt handler is running.
Triggering Event: Interrupts can happen completely asynchronously at any time and are triggered by hardware events, such as timers, external signals, or device I/O.
Preemption: Interrupt context preempts the currently running thread context.
Duration: Interrupt handlers are expected to execute quickly to minimize the delay from servicing the interrupt and to not block the execution of system threads and communication stacks threads.
Interrupts Nesting: Zephyr allows nested interrupts, meaning an interrupt handler can be interrupted by another interrupt of higher priority.
Allowed Operations:
Not Allowed Operations: