I believe that the most important aspect of embedded systems is the scheduler, which I think is also the main value of FreeRTOS. However, in addition to the debugger, ZephyrRTOS offers much more.
Main Thread
The ZephyrRTOS kernel starts the Main Thread after completing the necessary startup tasks, including the initialization of drivers, before entering the main function (https://docs.zephyrproject.org/latest/kernel/services/threads/system_threads.html).
As the program becomes more complex, including the complexity of the started drivers, the stack space of the Main Thread may become insufficient, leading to stack overflow errors in the system logs.
At this point, it is necessary to appropriately increase the stack space of the Main Thread by adding the following line to the prj.conf file (mentioned in the first article):
CONFIG_MAIN_STACK_SIZE=8192
to eliminate the error.
To display logs correctly, you also need to add the following line in prj.conf:
CONFIG_LOG=y
to ensure correct error log output. We will discuss the logging module later.
(According to the documentation) In Zephyr RTOS, the main function is executed after the kernel completes its initialization, and it can actually be absent or simply exit without causing an error. If the program crashes in the main method (thread aborts), it will lead to a fatal system error.
The Main Thread has a very high thread priority. By default, the Main Thread has the highest preemptible thread priority of 0 (highest configured preemptible thread priority); or, when preemptive multithreading support is not enabled in kernel options, it uses the lowest cooperative thread priority of -1.
Therefore, do not use busy loops in main that do not allow for sleep or mutexes, as they can block the execution of lower-priority threads.
The official recommendation is to create different application threads for different tasks, except for the simplest single-threaded applications.
Thread Priority
Official documentation: https://docs.zephyrproject.org/latest/kernel/services/threads/index.html
In Zephyr RTOS, there are two types of debugging modes supported (this is not a debugging algorithm). When the thread priority value is negative, this thread is a cooperative thread. This means that when this thread is the current thread (the thread being executed by the CPU), it will not be interrupted by other threads (note that this refers to threads, not interrupt IRQs) until this thread actively sets itself to “unready,” yielding execution rights. This includes but is not limited to sleep, yield, mutex wait, thread exit, thread suspend, etc.
Official documentation: https://docs.zephyrproject.org/latest/kernel/services/threads/index.html#id8
The following factors can cause a thread to be in an “unready” state:
-
The thread has not started;
-
The program is waiting for a kernel object to complete an operation (for example, acquiring a semaphore in an “unavailable” state);
-
The thread is waiting for a timeout to occur;
-
The thread is suspended;
-
The thread is terminated or aborted.
When the thread priority value is non-negative (greater than or equal to 0), the thread is a preemptive thread. When this thread is the current thread, it can be interrupted by any cooperative thread or by any preemptive thread with a higher or equal priority.
The kernel itself supports an unlimited number of thread priorities, controlled by:
CONFIG_NUM_COOP_PRIORITIES CONFIG_NUM_PREEMPT_PRIORITIES
These control the actual supported priority levels for the two thread modes in the application:
Cooperative threads range from -CONFIG_NUM_COOP_PRIORITIES (note the negative sign) to -1;
Preemptive threads range from 0 to CONFIG_NUM_PREEMPT_PRIORITIES-1.
ZephyrRTOS supports a special Meta-IRQ priority level, which will not be discussed here; interested readers can refer to the documentation.
User Mode Thread
When the SoC supports privilege levels, ZephyrRTOS provides user mode threads, which can provide lower privileges (limited by hardware), allowing kernel mode programs such as the Zephyr kernel itself, drivers, and kernel mode threads to be unaffected by user mode thread safety issues. We will discuss this in other articles; for now, we will focus on kernel mode threads.
Idle Thread
The idle thread is another system thread created by default to execute the lowest priority program and to allow the board’s power management system to enter power-saving mode when no other tasks are executing. The idle thread is an important thread; its failure can lead to fatal system errors.
Thread Creation
In the next issue, I plan to use UART operations as an example to discuss the use of the system subsystem (driver) API. For this, I will create a separate thread framework to prepare for the next issue.
ZephyrRTOS supports both static and dynamic thread creation. For simplicity, only static creation will be used here.
Based on the code from the first issue, i.e., without modifying nrf5340dk_nrf5340_cpuapp_ns.overlay (which should be empty) as done in the last issue,
zephyr,console=&uart0;
and without modifying prj.conf (which should also be empty) as done in this issue,
CONFIG_MAIN_STACK_SIZE
to let the program run correctly.
In the app1 (application directory), create an include directory and a file lte.h in this directory (to access LTE mode via serial port), with the following content:
#ifndef LTE_H#define LTE_H
#ifdef __cplusplus
extern "C" {
#endif
#define LTE_THREAD_STACK_SIZE (4 * 1024)
#define LTE_THREAD_PRIORITY 5
extern void lte_thread(void* p1, void* p2, void* p3);
#ifdef __cplusplus
}
#endif
#endif // LTE_H
Create a file lte.c in app1/src/ directory, with the following content:
#include <zephyr/kernel.h>
#include "lte.h"
void lte_thread(void* p1, void* p2, void* p3){
printk("lte thread started\n");
k_sleep(K_SECONDS(2));
printk("lte thread terminated\n");
}
Modify main.c to:
// Copyright (c) 2024 Qingdao IotPi Information Technology Ltd., All rights Reserved
#include <zephyr/kernel.h>
#include "lte.h"
K_THREAD_DEFINE(lte_tid, LTE_THREAD_STACK_SIZE, lte_thread, NULL, NULL, NULL, LTE_THREAD_PRIORITY, 0, 0);
int main(void){
printk("Hello World! %s\n", CONFIG_BOARD);
return 0;
}
Modify src/CMakeLists.txt to:
# Copyright (c) 2024 Qingdao IotPi Information Technology Ltd., All rights Reserved
target_sources(app PRIVATE main.c lte.c)
The program runs as follows:
In the above code,
void lte_thread(void* p1, void* p2, void* p3);
is the definition form of the thread function.
K_THREAD_DEFINE(lte_tid, LTE_THREAD_STACK_SIZE, lte_thread, NULL, NULL, NULL, LTE_THREAD_PRIORITY, 0, 0);
is the macro call for statically defining a thread. For documentation, see: https://docs.zephyrproject.org/latest/kernel/services/threads/index.html#c.K_THREAD_DEFINE
k_sleep(K_SECONDS(2));
is used to sleep the thread (do not use similar while((x–) == 0); code for delays!). The parameter for k_sleep is k_timeout_t and needs to be converted using macros like K_MSECS(), K_SECONDS(), K_MINUTES(), K_HOURS(), etc. https://docs.zephyrproject.org/latest/kernel/services/timing/clocks.html
Alternatively, you can use k_msleep(int32_t ms) or k_usleep(int32_t us) to sleep. Note that sleep time is related to the system clock, especially for k_usleep; if the system clock is not set correctly, the microsecond sleep time may not be accurate. Additionally, the time for waking from sleep is related to thread scheduling, and it may not wake up exactly after the set sleep time ends.
In the next issue, I plan to use three methods of system hardware abstraction (polling, interrupts, and asynchronous) to call the UART peripherals.