Designing Embedded Software Architecture: Task Scheduling

WeChat Official Account: Follow Orange Crazy Embedded to learn more practical skills in embedded programming. If you have any questions or suggestions, please leave a message on the official account; If you find this article helpful, feel free to like and follow.

1. Introduction

In the process of embedded MCU software development, the architecture of program task scheduling is particularly important, directly related to how many functions the program can support (as the number of functions increases, the system response ability weakens; a good task scheduling architecture can support more functions while maintaining the same system response ability). Below are three commonly used program task scheduling framework design schemes:

  • Sequential Execution Method

  • Time-Slicing Method

  • Operating System

2. Program Framework Design

Sequential Execution Method

This is a commonly used program framework design scheme for beginners, where not much consideration is needed; the code is simple, or the overall real-time and concurrency requirements of the system are not high. After initialization, it continuously calls the functions you have written in a while(1) { } or for(;;) { } loop, and generally does not consider the time required for each function execution. In most cases, there is some millisecond-level delay waiting in the functions.

Advantages: For beginners, this is the easiest and most intuitive program architecture, with simple and clear logic, suitable for software development with simple logic and relatively low complexity. Disadvantages: Low real-time performance. Since each function has some millisecond-level delays, even if it’s 1ms, it will cause different execution intervals for other functions. Although this can be managed through timer interrupts, the premise is that the time taken by the interrupt execution function must be short. When the program logic complexity increases, it can lead to confusion for later maintenance personnel, making it difficult to clarify the program’s running state.

Below is the main function code for a dormitory anti-theft system I did during my school days (there were some bugs at that time that were not resolved. Looking back now, there are actually many problems, and quite serious ones, such as a 3000ms delay in the interrupt service function, which is terrifying, as well as issues with serial port transmission, etc.; since the real-time requirements are not too high, the millisecond-level delays in the main function do not significantly affect system operation, except for the bugs; if maintenance is needed later, it would be a big project, better to rewrite):

 int main(void)
 {    
    u8 temperature;          
    u8 humidity;   
    int a;
    delay_init();
    uart2_Init(9600);     
    TIM3_Int_Init(4999,7199);
    ds1302_init();
    while(DHT11_Init());    //DHT11 initialization    
    a1602_init();        
    lcd12864_INIT();
    LcdInit();

    while(1)
    {
        for(a=0;a<11;a++)
        {
            num[a+3]=At24c02Read(a+2)-208;
            delay_us(10);                    
        }
        for(a=0;a<6;a++)
        {
            shuru[a]=At24c02Read(a+13)-208;
            delay_us(10);                
        }        
        delay_ms(10);  
        RED_Scan();
        Ds1302ReadTime();                    //Read ds1302 date and time
        shi=At24c02Read(0);                //Read alarm saved data
        delay_ms(10);
        fen=At24c02Read(1);                //Read alarm saved data            
        usart2_scan();                        //Bluetooth data scanning
        usart2_bian();                        //Bluetooth data processing
        nao_scan();
        k++;
        if(k<20)
        {
            if(k==1)
                LcdWriteCom(0x01);  //Clear screen
            LcdDisplay();                            //Display date and time
        }
        if(RED==0)
            RED_Scan();

        if(k>=20&&k<30)
        {
            if(k==20)
                LcdWriteCom(0x01);  //Clear screen
            Lcddisplay();                            //Display temperature and humidity
            LcdWriteCom(0x80+6);    
            DHT11_Read_Data(&temperature,&humidity);    //Read temperature and humidity values    
            Temp=temperature;Humi=humidity;
            LcdWriteData('0'+temperature/10);
            LcdWriteData('0'+temperature%10);
            LcdWriteCom(0x80+0X40+6);    
            LcdWriteData('0'+humidity/10);
            LcdWriteData('0'+humidity%10);
        }
        if(k==30)
            k=0;
        lcd12864();                                //Display anti-theft alarm status

    }        
}


//Timer 3 Interrupt Service Program
void TIM3_IRQHandler(void)   //TIM3 Interrupt
{
    int i;
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //Check if TIM3 update interrupt occurred
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //Clear TIMx update interrupt flag 
        if(key1==1&&FEN-fen==0&&SHI-shi==0)                //Alarm sounds when time is up
        {
            f=1;                        
        }
        else
        {
            f=0;
        }    
        if(USART_RX_BUF[0]=='R'&&USART_RX_BUF[1]=='I'&&USART_RX_BUF[2]=='N'&&USART_RX_BUF[3]=='G')
        {
            key0=1;
            for(i=0;i<17;i++)
            {
                USART_SendData(USART1, num[i]);//Send data to serial port 1
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//Wait for sending to finish
                USART_RX_STA=0;    
            }    
            delay_ms(3000);
            for(i=0;i<3;i++)
            {
                USART_SendData(USART1, num1[i]);//Send data to serial port 1
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//Wait for sending to finish
                USART_RX_STA=0;    
            }            
        }
    }
}

Time-Slicing Method

This is a program architecture design scheme that lies between the “Sequential Execution Method” and the “Operating System”. This design scheme is intended to help embedded software developers advance to the next level. In the process of embedded software development, if the following points are encountered, this design scheme can be considered the optimal choice, suitable for more complex embedded systems:

  1. The current design requirements do not necessitate the use of an operating system

  2. The task functions do not need to be executed constantly and have intervals (for example, buttons generally require software debouncing; beginners usually implement a delay of about 10ms before checking, but this wastes CPU resources significantly as the CPU could handle many other tasks during this time)

  3. There are certain real-time requirements

This design scheme requires the use of a timer, generally set to 1ms (the timer interval can be set arbitrarily, but too frequent interrupts reduce efficiency, and if the interrupt is too long, real-time performance suffers). Therefore, it is necessary to consider the execution time of each task function, which should not exceed 1ms (it is best to optimize the execution time through programming; if optimization is not possible, the execution cycle of the task must be significantly greater than the time consumed by the task). Additionally, there should be no millisecond-level delays in the main loop or task functions.

How to determine the task cycle for each function? It depends on the task’s duration and effect. For example, the button scanning task cycle is 10ms (to improve responsiveness), the indicator light control task cycle is 100ms (usually a maximum flashing frequency of 100ms is just right, excluding special requirements), and the LCD/OLED display cycle is 100ms (the time taken through interfaces like SPI/IIC is about 1-10ms, or even longer, so the task cycle must be significantly greater than the time taken, while also not being too long to meet the visual refresh rate, making 100ms a suitable cycle).

Below are two different implementation schemes, one for those unfamiliar with function pointers and another for those who want to learn further.

① Design Without Function Pointers

  1. First, define a timing flag variable with a 1 millisecond time slice, accumulate time in the timer interrupt function, and set the corresponding time flags to 1.

void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
    {
        sg_1msTic++;

        sg_1msTic % 1 == 0 ? TIM_1msFlag = 1 : 0;
        sg_1msTic % 10 == 0 ? TIM_10msFlag = 1 : 0;
        sg_1msTic % 20 == 0 ? TIM_20msFlag = 1 : 0;
        sg_1msTic % 100 == 0 ? TIM_100msFlag = 1 : 0;
        sg_1msTic % 500 == 0 ? TIM_500msFlag = 1 : 0;
        sg_1msTic % 1000 == 0 ? (TIM_1secFlag  = 1, sg_1msTic = 0) : 0;
    }

    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
  1. Then in the main function loop, check the time flags; if a flag is set to 1, it indicates that the time is up, and the corresponding function can be executed. After all functions have been executed, reset the flag to 0 and wait for the next trigger.

int main(void)
{
    System_Init();

    while (1)
    {
        if (TIM_1msFlag)
        {
            CAN_CommTask();// CAN communication task

            TIM_1msFlag = 0;
        }

        if (TIM_10msFlag)
        {
            KEY_ScanTask();// Button scanning task
            Hmi_Task();// Human-machine interaction task

            TIM_10msFlag = 0;
        }

        if (TIM_100msFlag)
        { 
            LED_CtrlTask();// Indicator light task

            TIM_100msFlag = 0;
        }    

        if (TIM_1secFlag)
        {
            WDog_Task();// Dog feeding task

            TIM_1secFlag = 0;
        }  
    }
}

② Design With Function Pointers

  1. Define a structure containing function pointers to point to the functions that need to be executed periodically.

typedef struct{
    uint8 m_runFlag;/*!< Program running flag: 0-not running, 1-running */
    uint16 m_timer;/*!< Timer */
    uint16 m_itvTime;/*!< Task running interval time */
    void (*m_pTaskHook)(void);/*!< Task function to be executed */
} TASK_InfoType; 
  1. Implement the basic scheduling function of task pointers: task scheduling management and task scheduling execution.

/**
  * @brief      Task function running scheduling management.
  */
void TASK_Remarks(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
        if (sg_tTaskInfo[i].m_timer)
        {
            sg_tTaskInfo[i].m_timer--;

            if (0 == sg_tTaskInfo[i].m_timer)
            {
                 sg_tTaskInfo[i].m_timer = sg_tTaskInfo[i].m_itvTime;
                 sg_tTaskInfo[i].m_runFlag = 1;
            }
        }
   }
}

/**
  * @brief      Task function scheduling execution.
  */
void TASK_Process(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
        if (sg_tTaskInfo[i].m_runFlag)
        {
             sg_tTaskInfo[i].m_pTaskHook(); // Run task
             sg_tTaskInfo[i].m_runFlag = 0; // Clear flag
        }
    }   
}
  1. Call task scheduling management and task scheduling execution in the timer interrupt function and the main function respectively.

int main(void)
{
    System_Init();

    while (1)
    {
        TASK_Process();
    }
}

/**
  * @brief      Timer 3 interrupt service function.
  */
void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
    {
        TASK_Remarks();
    }

    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
  1. Simply add the functions that need to be executed.

#define TASKS_MAX     5    // Define the number of tasks

/** Task function related information */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
    {0, 1, 1, CAN_CommTask},     // CAN communication task
    {0, 10, 10, KEY_ScanTask},   // Button scanning task
    {0, 10, 10, LOGIC_HandleTask}, // Logic processing task
    {0, 100, 100, LED_CtrlTask},// Indicator light control task
    {0, 1000, 1000, WDog_Task},  // Dog feeding task
};

Operating System

The embedded operating system EOS (Embedded Operating System) is a widely used system software. In the past, it was mainly applied in the fields of industrial control and national defense systems. For microcontrollers, commonly used preemptive operating systems include UCOS, FreeRTOS, RT-Thread Nano, and RTX (other operating systems like Linux are not suitable for microcontrollers).

In terms of task execution, the operating system and the “Time-Slicing Method” do not have excessive requirements on the time consumed by each task; it is necessary to set the priority of each task, and when a high-priority task is ready, it will preempt a low-priority task. The operating system is relatively complex, so it is not detailed here. Regarding how to choose a suitable operating system (comparison of characteristics of RTOS such as [RTOS]uCOS, FreeRTOS, RTThread, RTX, etc.): UCOS: abundant online resources, very suitable for learning, but requires payment for product use. FreeRTOS: free to use, hence widely used in many products. RT-Thread: a domestic IoT operating system with a rich set of components, also free, see: RT-Thread Document Center. RTX: a royalty-free, deterministic real-time operating system designed for ARM and Cortex-M devices.

Here is a comparison chart found online:

Designing Embedded Software Architecture: Task Scheduling

3. Conclusion

From the above comparison, it can be seen that the time-slicing polling method has significant advantages. It combines the benefits of both the sequential execution method and the operating system. The structure is clear, simple, and very easy to understand, making it a commonly used microcontroller design framework.

Designing Embedded Software Architecture: Task Scheduling

Leave a Comment

×