Understanding the Essence of a Small Embedded Operating System

Understanding the Essence of a Small Embedded Operating System

Understanding the Essence of a Small Embedded Operating System

This is a document I found online, and the author’s goal is to design a simple embedded operating system that only implements the functionality of a basic task scheduler. As the author mentioned, although it cannot be called an operating system, it embodies the essence of a small embedded operating system. It serves as excellent learning material for us to gain insight into the nature of operating systems, and I would like to share it with everyone today.

1

Multitasking Mechanism

In fact, under a single CPU, there is no real multitasking mechanism; only different tasks take turns using the CPU, so it is essentially still single-tasking. However, due to the extremely fast execution speed of the CPU, combined with the frequent and rapid task switching, we feel as if many tasks are running simultaneously. This is known as the multitasking mechanism.

The characteristic of a real-time system is predictable latency, allowing it to respond to certain signals within a specified time (usually on the order of milliseconds).

2

Task States

Tasks have the following characteristics: a task cannot run at all times, and a task that is already running cannot guarantee to occupy the CPU until it finishes running. Generally, there are ready state, running state, suspended state, etc.

Running State: A task in the running state is a task that is currently using the CPU. At any moment, there can only be one running task.

Ready State: A task in the ready state is runnable, waiting for the CPU to be released by the currently running task.

Suspended State: A state where certain conditions are not met, causing the task to be unable to run.

3

How to Transition to Ready State

INT32U OSRdyTbl; /* Ready Task Table */

The above defines a 32-bit variable, where each bit represents a task; 0 indicates a suspended state, and 1 indicates a ready state. It records the readiness status of each task, referred to as the ready table. OSRdyTbl is defined as a 32-bit variable, corresponding to 32 tasks. Of course, if defined as 64 bits, it can support up to 64 tasks. This way, two macros can be defined to change the task’s state to ready or suspended.

/* Register ready tasks in the ready table */

#define OSSetPrioRdy(prio) { OSRdyTbl |= 0x01<<prio;} // Set the corresponding bit to 1

/* Remove tasks from the ready table */

#define OSDelPrioRdy(prio) { OSRdyTbl &= ~(0x01<<prio); }// Clear the corresponding bit

Tasks are independent of each other, and there is no mutual calling relationship. All tasks are logically equal. Since tasks cannot see each other, information transmission between them cannot be completed face-to-face. This requires various communication mechanisms such as semaphores, message mailboxes, queues, etc.

4

What is Preemptive Scheduling?

The concept of scheduling, in simple terms, is the system’s selection of an appropriate task to execute among multiple tasks. How does the system know when to execute which task? A unique priority level can be assigned to each task, and when multiple tasks are ready at the same time, the task with the higher priority runs first. At the same time, the task’s priority also serves as its unique identifier. Operations on tasks in the code are all completed based on these identifiers.

Preemptive scheduling refers to: once a higher-priority task appears in the ready state, it immediately deprives the current task of its running rights and allocates the CPU to the higher-priority task. Thus, the CPU always executes the task with the highest priority that is in the ready condition.

5

Time Management in Multitasking Systems

Like humans, multitasking systems also need a “heartbeat” to maintain their normal operation, which is called the clock tick, usually generated by a timer that produces a fixed-period interrupt.

The OSTimeDly function is based on the clock tick for delay (in the clock’s interrupt service function, each delayed task’s delay tick count is decremented by 1. If a task’s delay tick count becomes 0, it transitions from the suspended state to the ready state.). This function performs a very simple function: it first suspends the current task, sets its delay tick count, and then performs a task switch. Once the specified clock tick count arrives, the current task is restored to the ready state. Tasks must yield the CPU’s usage rights through OSTimeDly or OSTaskSuspend (delay or wait for an event) to allow lower-priority tasks the opportunity to run.

6

How to Implement Multitasking?

With only one CPU, how can multiple independent programs run at the same time? To achieve multitasking, the condition is that each task is independent of each other. How can a person be independent, having their own private property? The same goes for tasks; if a task has its own CPU, stack, program code, and data storage area, then that task is an independent task. (The CPU is obtained through the multitasking mechanism; the rest needs to be allocated by you.)

TIPS:

If a task is running a public function (like Printf) and is preempted by another higher-priority task, it is very likely that when this higher-priority task also calls the same public function, it will corrupt the data of the original task. This is because two tasks may share the same set of data. To prevent this from happening, two measures are commonly adopted: reentrant design and mutual exclusion calls.

In a reentrant function, all variables are local variables, and local variables temporarily allocate space during the call. Therefore, when different tasks call the function at different times, the storage space allocated for the same local variable does not overlap (in the task’s private stack), thus avoiding interference. Additionally, if a reentrant function calls other functions, those called functions must also be reentrant functions.

Methods related to interruptions, disabling scheduling, mutex semaphores, counting semaphores, etc., are used to achieve mutual (exclusive) access.

6.1 How Does a Task Own Its Program Code?

To achieve multitasking, the first thing is the program code. Each task’s program code is like a function, similar to the bare program of 51, where each task is a large loop. Then there is the data storage area. Since global variables are shared by the system, shared by various tasks, they are not private to tasks. Therefore, the data storage area here refers to the task’s private variables. How to make them private? Local variables also. The compiler saves local variables in the stack, so it’s simple; as long as the task has a private stack, it will work.

TIPS:

Critical resources are shared resources that only allow one task to use at a time. The piece of code that accesses critical resources in each task is called the critical section.

In a multitasking system, to ensure the reliability and integrity of data, shared resources must be accessed mutually (exclusively), so global variables (except for read-only ones) cannot be accessed by multiple tasks at the same time, meaning when one task is accessing, it cannot be interrupted by other tasks. Shared resources are a type of critical resource.

6.2 How Does a Task Own Its Stack and Data Storage Area?

The private stack serves to store local variables and function parameters. It is a linear space, so a static array can be allocated, pointing the stack pointer SP to the first element of the stack array (increasing stack) or the last element (decreasing stack). This creates an artificial stack. Each task also needs a variable to record its stack pointer, stored in the task control block (TCB).

What is a task control block?

Each task in the system has a task control block, which records the execution environment of the task. This task control block is relatively simple, containing only the task’s stack pointer and task delay tick count. The task control block is the task’s identity card. It links the task’s program with its data, allowing you to find all resources of the task.

Understanding the Essence of a Small Embedded Operating System

6.3 How Does a Task Own Its CPU?

Finally, let’s look at how a task “owns” its CPU. With only one CPU, all tasks share it, taking turns to use it. How can this be achieved? First, let’s look at the interruption process. When an interrupt occurs, the CPU saves the current program’s running address, registers, and other contextual data (usually saved in the stack), and then jumps to the interrupt service program to execute. Once finished, the previously saved data is restored to the CPU, returning to the original program execution. This achieves the interleaved execution of two different programs.

Doesn’t this idea allow for multitasking? By mimicking the interruption process, task switching can be achieved. During task switching, the current task’s contextual data is saved in its task stack, and the data of the task to be run is loaded from its task stack into the CPU, changing the CPU’s PC, SP, registers, etc. In other words, task switching is the switching of the task’s running environment. The task’s running environment is stored in the task stack, meaning the key to task switching is assigning the task’s private stack pointer to the processor’s stack pointer SP.

Understanding the Essence of a Small Embedded Operating System

Creating a task involves three parameters: the task’s entry address, the starting address of the task stack, and the task’s priority. After calling this function, the system initializes the task stack based on the parameters provided by the user and saves the stack pointer in the task control block, marking the task as ready in the task ready table. Finally, it returns, successfully creating a task.

When a task is about to run, it retrieves its stack pointer (stored in the task control block) to push the registers into the corresponding locations in the CPU.

6.4 How to Achieve Preemptive Scheduling?

Preemptive scheduling based on task priority means that as soon as the highest priority task is in the ready state, it immediately preempts the processor resources of the currently running low-priority task. To ensure that the CPU always executes the highest priority task in the ready condition, every time the task’s state changes, it checks whether the currently running task is the highest priority task among the ready tasks; otherwise, it performs a task switch.

When does the task state change? There are two situations:

1. A high-priority task actively requests suspension to yield the processor due to needing some resources or delays, at which point the low-priority task in the ready state is scheduled to execute. This type of scheduling is called task-level switching. For example, a task executing OSTimeDly() or OSTaskSuspend() belongs to this.

2. A high-priority task, due to the arrival of a clock tick or the end of interrupt processing, the kernel finds that a higher-priority task has obtained execution conditions (like the clock timing out), then directly switches to the higher-priority task after the interrupt. This type of scheduling is also called interrupt-level switching.

6.5 Suspending/Resuming Tasks

1. Suspending a Task

Through OSTaskSuspend(), a task can actively suspend itself. OSTaskSuspend() removes the task from the task ready table and finally restarts the system scheduling. This function can suspend the task itself or other tasks.

2. Resuming a Task (OSTaskResume())

This allows tasks that have been suspended by OSTaskSuspend or OSTimeDly to resume to the ready state, followed by task scheduling.

Understanding the Essence of a Small Embedded Operating System

1. Is Fourier Transform really that simple? Have you learned it?

2.Can hardware defects really be patched with software? Let’s review the software fixes for hardware issues.

3. To be honest, engineers are the coolest profession on this planet!

4.Do you know where each variable is stored in embedded C language?

5. The mutual disdain between analog electronics and digital electronics led to a hilarious outcome.

6.As an electronic engineer, learning to read datasheets is crucial!

Understanding the Essence of a Small Embedded Operating System

Disclaimer: This article is a network repost, and the copyright belongs to the original author. If there are copyright issues, please contact us, and we will confirm the copyright based on the materials you provide and pay royalties or delete the content.

Leave a Comment

Your email address will not be published. Required fields are marked *