Sharing Years of Experience in Embedded Programming: A Different Perspective on Programming

1. Limitations of Traditional Programming Structures

When not using an RTOS, embedded software typically employs two traditional programming structures: one is called the “front-back structure” or “super loop structure,” which is essentially an event-driven programming method; the other is a time-triggered programming method, such as Michael J. Pont’s “time-triggered programming model.”In practical work, when the system becomes slightly complex, it becomes evident that both methods have certain limitations. Below, I will illustrate this with a problem encountered in an actual product design.In designing a wall-mounted smart power distribution instrument for a distribution cabinet, the CPU program design must accomplish the following tasks:

(1) Refresh the display data on the front screen every half second.

(2) Refresh the DI/DO every 0.1 seconds.

(3) Scan the keyboard every 0.2 seconds.

(4) Recollect and calculate measurement data every half second.

(5) Communicate with the host computer via Modbus over an asynchronous serial port, with a maximum rate of 19200 bps.

(6) Communicate with the clock chip and EEPROM via the IIC bus.

(7) Communicate with the LED digital tube and acquisition chip via the SPI bus.

(8) Perform FFT transformation on the collected 6 channels of signals.

(9) When the system loses power, the CPU must quickly respond to write the current power meter reading into the EEPROM.

Among the above tasks, tasks (5) and (9) are strongly real-time. If the serial port’s send/receive events do not receive timely responses, it can lead to byte loss during reception and excessive time intervals during sending, causing Modbus frame delimitation errors on the receiving end. If the system does not respond promptly to power loss events, it may result in EEPROM write failures.Other tasks only need to be executed within the specified cycle, but task (8) is somewhat special. Using a typical 8-bit CPU to perform FFT transformations on 6 types of signals, even if each signal only performs a 128-point FFT, it takes several seconds to compute once.Now, let’s look at the challenges encountered when implementing the above design using traditional programming structures. 1.1 Programming Using the “Front-Back Structure”When programming using the “front-back structure,” to ensure the timeliness of task (5), UART interrupts are used. When the UART completes the transmission of a byte, it generates an interrupt. In the interrupt routine, the received character is stored in the receive buffer or the next character to be sent is taken from the send buffer and loaded into the UART for transmission. The handling of the Modbus protocol can be processed separately in a task outside the interrupt, ensuring that the interrupt routine remains short.To ensure the timely response of task (9), it must also be assigned an interrupt.When the system loses power, there is less than 10 ms of transition time. If the system cannot complete the relevant operations within this time, the system voltage will drop below the effective voltage, losing its ability to function.After arranging the background interrupt tasks, let’s see how to complete the foreground tasks.The biggest challenge encountered here is handling task (8), as the execution time required for task (8) is too long. Simply treating it as a single task will affect the system’s response to other tasks. The code structure in the super loop is as follows:

while(1)

{

Task (1);

Task (2);

………

Task (8);

}

Since task (8) takes several seconds to execute once, the entire super loop takes at least longer than the time required for task (8). This means that the super loop takes several seconds to complete one iteration, which does not meet the response time requirements for each task.To solve this problem, task (8) must be split into many subtasks, compressing the execution time of each subtask to around 10 milliseconds, and defining the state after each subtask is completed. In the super loop, only one subtask is executed each time based on the state, and the program structure is as follows:

while(1)

{

Task (1);

Task (2);

………

Switch (subtask state)

{

case subtask state 1:

Subtask 1;

break;

case subtask state 2:

Subtask 2;

break;

…………

case subtask state n:

Subtask n;

break;

}

}

Thus, a task that takes several seconds for FFT computation must be split into hundreds of subtasks, each taking about 10 ms, which is clearly unacceptable.In addition, the super loop structure has an implicit drawback: as the number of tasks increases, the execution time of the loop body increases linearly. In actual design, even without high-cost tasks like task (8), ensuring the system’s timely response as functionality increases is also a significant challenge. 1.2 Programming Using the “Time-Triggered Programming Model”The core of the “time-triggered programming model” is to establish a cooperative task scheduler based on time triggers, minimizing event triggers (reducing the use of interrupts). The system completes the scheduling and execution of tasks through the task scheduler. Below is a typical program structure of the “time-triggered programming model”:

/*——————–Main Function———————–*/

Void main(void)

{

SCH_Init();// Set up the scheduler

SCH_Add_Task(task function name, task scheduling delay, task scheduling period);// Add the task to the scheduler’s task queue

SCH_Start();// Refresh the task queue

while(1)

{

SCH_Dispatch_Tasks(); // Execute the task scheduler

}

}

/*——————-Timer Interrupt Function———————*/

Void SCH_Update(void) interrupt

{

// Refresh the task queue

}

Each task in the system is defined with a priority, task cycle period, and task delay time. The system timer interrupt program SCH_Update() refreshes the task queue at the set pace. In the super loop, only the task scheduler SCH_Dispatch_Tasks() is executed, arranging the execution of tasks based on the state of the task queue.This programming structure avoids the problem of the super loop structure’s execution time increasing linearly with the amount of code. However, since tasks are non-preemptive, once a task starts executing, the task scheduler only has the opportunity to execute after the current task is completed. This requires that each task does not occupy the CPU for too long; otherwise, it will affect the overall system response speed.Therefore, FFT computation must still be effectively split in this programming model; otherwise, it will require upgrading the CPU or using a preemptive RTOS, which will inevitably increase system costs.So, is there a better solution?The following programming structure improves the “time-triggered programming model” to allow programmers to define tasks more intuitively without increasing hardware costs, reducing the impact of task characteristics on the system program structure, simplifying the program structure, and improving the system’s real-time response speed.2. Improvements to the “Time-Triggered Programming Model”Based on years of experience in embedded system programming, tasks in embedded systems can typically be divided into three types:

(1) Timely tasks;

(2) Periodic tasks;

(3) Background tasks;

Characteristics of Timely Tasks: These tasks are event-triggered; once an event occurs, the system must respond within a specified time. The most natural way to handle these tasks is to use interrupts, defining them as background tasks in the “front-back structure.”Characteristics of Periodic Tasks: These tasks are time-triggered and periodic; the system must ensure that tasks are executed within the specified period. The “time-triggered programming model” can meet the needs of these tasks well.Characteristics of Background Tasks: These tasks are non-real-time; timeliness is not very important. The system can interrupt these tasks at any time during operation to execute the first two types of tasks. The system only needs to utilize resources to complete these tasks as quickly as possible. These tasks are best defined as foreground tasks in the “front-back structure.”Based on the above task classification, the improvements to the “time-triggered programming model” can be summarized as follows:

(1) Tasks are divided into three categories, with category 1 having the highest priority and category 3 having the lowest;

(2) High-priority tasks can interrupt the execution of low-priority tasks, while tasks of the same priority cannot preempt each other.

(3) In actual design, to improve the system’s predictability, the number of category 1 tasks and their execution time should be minimized.

(4) To reduce system resource usage, the system does not allocate separate stack space for tasks.

The essence of the above improvements is to design a simple task scheduling mechanism with three priorities. High-priority tasks can interrupt low-priority tasks, while tasks of the same priority cannot preempt each other. This scheduling mechanism does not save the task context and separate stack for each individual task, thus reducing the resource requirements of this programming model.In a preemptive RTOS, when a high-priority task interrupts a low-priority task, it saves the context of the low-priority task and stores its local variables in a separate stack. If the system does not allocate separate stacks for tasks, how can it ensure the recovery of the low-priority task’s execution environment after the high-priority task exits?This issue can be addressed by borrowing the interrupt handling mechanism with the following approach:

(1) Design a timer interrupt function in the system, which is responsible for executing periodic task scheduling. This timer interrupt has the lowest priority among all interrupts;

(2) Design another timer interrupt function in the system, which is responsible for refreshing the task management queue of periodic tasks, providing support for task scheduling. This timer interrupt function has the second-lowest priority in the system;

(3) A periodic task is a function where the first operation at the entry is to enable interrupts (Question: This interrupt refers to the interrupt that triggers timely tasks; should it be enabled or disabled outside the periodic task? If enabled,)allow the task to be interrupted during execution to respond to timely tasks.

(4) Background tasks are the code executed in the main function’s super loop, which can be interrupted at any time by timely and periodic tasks. The code for background tasks only executes when there are no timely and periodic tasks.

Through the above measures, the program structure of the “improved time-triggered programming model” is as follows:

/*——————–Main Function———————–*/

Void main(void)

{

SCH_Init();// Set up the scheduler

SCH_Add_Task(task function name, task scheduling delay, task scheduling period);// Add the task to the scheduler’s task queue

SCH_Start();// Refresh the task queue

while(1)

{

Background task 1;

………

Background task n;

}

}

/*——————-Second-Lowest Priority Timer Interrupt Function———————*/

Void SCH_Update(void) interrupt

{

// Refresh the task queue

}

/*——————-Lowest Priority Timer Interrupt Function———————*/

Void SCH_Dispatch_Tasks(void) interrupt

{

// Schedule periodic tasks

}

/*——————-Typical Structure of Periodic Tasks———————*/

Void SCH_Cycle_Task1(void)

{

// Enable interrupts /* This function can rely on interrupts to trigger timely tasks */

// Execute task

return; // Task returns

}

ConclusionUsing the “improved time-triggered programming model” for small embedded system programming is akin to programming with an RTOS. Once designers plan the tasks, they can focus on the design of each task. The system can manage the CPU time occupied by tasks uniformly, reducing coupling between tasks, making product program design and modifications clear and straightforward.This programming model effectively addresses the complex design issues faced by the wall-mounted smart power distribution instrument, proving that this method is simple and effective.Currently, this design model has only established a task scheduler, and variable passing between tasks still requires the use of global variables. If semaphore and messaging mechanisms can be added, this model will be even more complete, making programming for low-cost small embedded systems more convenient and clear.-END-

Leave a Comment