From 0 to 1 Write RT-Thread Kernel: Implementation of Free Thread and Blocking Delay

Posted by freedmania on Sun, 08 Sep 2019 11:38:35 +0200

In another previous article, <From 0 to 1 Write RT-Thread Kernel-Implementation of Thread Definition and Switching], the delay in the thread body uses software delay, that is, to make the CPU empty and so on to achieve the effect of delay. Delays in RTOS are called blocking delays, that is, when threads need to delay, threads will give up the use of CPU. CPU can do other things. When threads delay, the CPU can regain the use of CPU, and threads continue to run, which makes full use of CPU resources instead of waiting.

When a thread needs to be delayed and blocked, if no other thread can run, RTOS will create an idle thread for the CPU, and then the CPU will run the idle thread. In RT-Thread, idle threads are the lowest priority threads created when the system is initialized. The idle threads mainly do some system memory cleaning. But for simplicity, our idle thread counts only one global variable. In practical applications, when the system enters idle threads, it can let MCU enter dormancy or low power operation in idle threads.

We divide the implementation of idle threads and blocking delays into the following two steps:

1. Implementing idle threads

1. Define the stack of idle threads

#include <rtthread.h>
#include <rthw.h>
#define IDLE_THREAD_STACK_SIZE      512 
ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t rt_thread_stack[IDLE_THREAD_STACK_SIZE];

2. Define idle thread control blocks

struct rt_thread idle;

3. Define idle thread functions

rt_ubase_t  rt_idletask_ctr = 0;

void rt_thread_idle_entry(void *parameter)
{
    parameter = parameter;
    while (1)
    {
        rt_idletask_ctr ++;
    }
}

4. Initialization of idle threads

/**
 * @ingroup SystemInit
 *
 * Initialize idle threads and start idle threads
 *
 * @note This function must be called when the system is initialized
 */
void rt_thread_idle_init(void)
{
    
    /* Initialization thread */
    rt_thread_init(&idle,
                   "idle",
                   rt_thread_idle_entry,
                   RT_NULL,
                   &rt_thread_stack[0],
                   sizeof(rt_thread_stack));
    
	/* Insert threads into the ready list */
    rt_list_insert_before( &(rt_thread_priority_table[RT_THREAD_PRIORITY_MAX-1]),&(idle.tlist) );
}

The above four steps have been described in detail in previous articles -- <from 0 to 1 write RT-Thread Kernel --- Thread Definition and Implementation of Switching > and will not be explained too much here.

2. Achieving blocking delay

Blocking delay is when a thread calls the delay function, the thread will be stripped of CPU usage rights, and then enter the blocking state. Until the end of the delay, the thread will regain CPU usage rights before it can continue to run. During the period of thread blocking, the CPU can execute other threads, if other threads do the same. In the delayed state, the CPU will run idle threads. We define a delay function rt_thread_delay, whose code list is as follows:

In the above code, we use thread - > remaining_tick to judge whether a thread's delay ends or not. thread - > remaining_tick decreases in SysTick_Handler. The code is as follows:

void SysTick_Handler(void)
{
    /* Entry interruption */
    rt_interrupt_enter();

    rt_tick_increase();

    /* Departure interruption */
    rt_interrupt_leave();
}
/* 
 *    rt_interrupt_nest The interrupt counter is a global variable that records the number of interrupt nests.
 *    Every time you enter an interrupt function, you add one
 *    Every time you leave an interrupt function, you subtract one
 */
volatile rt_uint8_t rt_interrupt_nest;

/**
 * The interrupt service function of the BSP file is called when it enters.
 * 
 * @note Do not call this function in an application
 *
 * @see rt_interrupt_leave
 */
void rt_interrupt_enter(void)
{
    rt_base_t level;
    
    
    /* Closing interruption */
    level = rt_hw_interrupt_disable();
    
    /* Interrupt counter + */
    rt_interrupt_nest ++;
    
    /* Open and interrupt */
    rt_hw_interrupt_enable(level);
}


/**
 * The interrupt service function of the BSP file is called when it leaves.
 *
 * @note Do not call this function in an application
 *
 * @see rt_interrupt_enter
 */
void rt_interrupt_leave(void)
{
    rt_base_t level;
    
    
    /* Closing interruption */
    level = rt_hw_interrupt_disable();
    
    /* Interrupt counter-- */
    rt_interrupt_nest --;

    /* Open and interrupt */
    rt_hw_interrupt_enable(level);
}
//rt_tick, a system time base counter, is a global variable that records how many SysTick interrupts were generated.
static rt_tick_t rt_tick = 0;
extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];


void rt_tick_increase(void)
{
    rt_ubase_t i;
    struct rt_thread *thread;
    rt_tick ++;

	/* Scan the remaining_tick of all threads in the ready list, if not 0, minus 1 */
	for(i=0; i<RT_THREAD_PRIORITY_MAX; i++)
	{
        thread = rt_list_entry( rt_thread_priority_table[i].next,
								 struct rt_thread,
							     tlist);
		if(thread->remaining_tick > 0)
		{
			thread->remaining_tick --;
		}
	}
    
    /* System Scheduling */
	rt_schedule();
}

Next, let's look at the main function and the function bodies of thread 1 and thread 2. In the main function, we initialize idle threads, thread 1 and thread 2 and insert them into the list of corresponding ready lists. Then we start system scheduling (rt_system_scheduler_start), which starts from thread 1.( If you don't understand this, you can read another blog: <From 0 to 1 Write RT-Thread Kernel - Thread Definition and Switching Implementation> If a thread is blocked, run another thread, and if non-idle threads are blocked, run idle threads.

/************************************************************************
  * @brief  main function
  * @param  nothing
  * @retval nothing
  *
  * @attention
  *********************************************************************** 
  */
int main(void)
{	
	/* Hardware Initialization */
	/* Put hardware-related initialization here, and if it is a software simulation, there is no relevant initialization code. */
	
    /* It is a good habit to turn off interrupts at the beginning of a program. When the system is initialized, the thread is created and the system is started.                                        
     * Interrupts are reopened during scheduling (interrupts are reopened and interrupt labels are set in the rt_hw_context_switch_to function).
     * If interrupts are not turned off at first, SysTick initialization is completed, then the system is initialized and threads are created, if the system is initialized
     * And if the thread creation time is longer than the SysTick interrupt cycle, then the system or thread will be executed before it is ready.
     * SysTick interrupt service function is executed, and system scheduling is performed in this function, which shows that this is unreasonable.
     */
    rt_hw_interrupt_disable();
    
    /* Initialize SysTick and call the firmware library function SysTick_Config.
     * Configuration interrupt cycle is 10 ms (100), interrupt priority is the lowest
     */
    SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND );
	
	/* Scheduler initialization */
	rt_system_scheduler_init();

    /* Initialize idle threads */    
    rt_thread_idle_init();	
	
	/* Initialization Thread 1 */
	rt_thread_init( &rt_flag1_thread,                 /* Thread Control Block */
                    "rt_flag1_thread",                /* Thread name, string form */
	                flag1_thread_entry,               /* Thread entry address */
	                RT_NULL,                          /* Thread parameter */
	                &rt_flag1_thread_stack[0],        /* Thread stack start address */
	                sizeof(rt_flag1_thread_stack) );  /* Thread stack size in bytes */
	/* Insert threads into the ready list */
	rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
	
	/* Initialization Thread 2 */
	rt_thread_init( &rt_flag2_thread,                 /* Thread Control Block */
                    "rt_flag2_thread",                /* Thread name, string form */
	                flag2_thread_entry,               /* Thread entry address */
	                RT_NULL,                          /* Thread parameter */
	                &rt_flag2_thread_stack[0],        /* Thread stack start address */
	                sizeof(rt_flag2_thread_stack) );  /* Thread stack size in bytes */
	/* Insert threads into the ready list */
	rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );
	
	/* Start System Scheduler */
	rt_system_scheduler_start(); 
}

 

/* Thread 1 */
void flag1_thread_entry( void *p_arg )
{
	for( ;; )
	{
#if 0
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* Thread switching, here is manual switching */		
		rt_schedule();
#else
        flag1 = 1;
        rt_thread_delay(2); 		
		flag1 = 0;
        rt_thread_delay(2);
#endif        
	}
}

/* Thread 2 */
void flag2_thread_entry( void *p_arg )
{
	for( ;; )
	{
#if 0
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
		
		/* Thread switching, here is manual switching */
		rt_schedule();
#else
        flag2 = 1;
        rt_thread_delay(2); 		
		flag2 = 0;
        rt_thread_delay(2);
#endif        
	}
}

The results of the program are as follows. In fact, the blocking delay is very similar to the "front-end and back-end polling" used in bare MCU.

Finally, I just want to make a summary of the learning points. Most of the knowledge in this article comes from the book "RT-Thread Kernel Implementation and Application Development Practice-STM32" published by Wildfire Company. This book is very good. Those who are interested in learning the RT-Thread Internet of Things operating system can consider it.