从0到1写RT-Thread内核——空闲线程与阻塞延时的实现

       在之前写的另外一篇文章——<从0到1写RT-Thread内核——线程定义及切换的实现>中线程体内的延时使用的是软件延时,即还是让CPU空等来达到延时的效果。RTOS中的延时叫阻塞延时,即线程需要延时的时候,线程会放弃CPU的使用权,CPU可以去干其他的事情,当线程延时时间到,重新获取CPU使用权,线程继续运行,这样就充分利用了CPU的资源,而不是干等着。

       当某个线程需要延时,进入阻塞状态,如果没有其他线程可以运行,RTOS都会为CPU创建一个空闲线程,这个时候CPU就运行空闲线程。在RT-Thread中,空闲线程是系统在初始化的时候创建的优先级最低的线程,空闲线程主要是做一些系统内存的清理工作。但为了简单起见,这里我们的空闲线程只对一个全局变量进行计数。在实际应用中,当系统进入空闲线程的时候,可在空闲线程中让单片机进入休眠或者低功耗等操作。

我们把空闲线程与阻塞延时的实现分为以下两大步:

一.实现空闲线程

1.定义空闲线程的栈

#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.定义空闲线程控制块

struct rt_thread idle;

3.定义空闲线程函数

rt_ubase_t  rt_idletask_ctr = 0;

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



        /* 进行系统调度,这个是我自己后来加的,野火官方的例程是没有调用rt_schedule,        
         *我觉得那样是错误的,因为这里不加rt_schedule的话程序执行到空闲线程就回不去了
         */
	    //rt_schedule();//这里补充一下,野火的例程并没有错,每次产生滴答定时器中断都会调用调度器,所以这里不需要调用调度器的
    }
}

4.空闲线程初始化

/**
 * @ingroup SystemInit
 *
 * 初始化空闲线程,启动空闲线程
 *
 * @note 当系统初始化的时候该函数必须被调用
 */
void rt_thread_idle_init(void)
{
    
    /* 初始化线程 */
    rt_thread_init(&idle,
                   "idle",
                   rt_thread_idle_entry,
                   RT_NULL,
                   &rt_thread_stack[0],
                   sizeof(rt_thread_stack));
    
	/* 将空闲线程插入到就绪列表中优先级最低的链表中 */
    rt_list_insert_before( &(rt_thread_priority_table[RT_THREAD_PRIORITY_MAX-1]),&(idle.tlist) );
}

以上4步在之前的文章——<从0到1写RT-Thread内核——线程定义及切换的实现>有详细介绍过了,这里就不再过多解释。

二.实现阻塞延时

      阻塞延时的阻塞是指线程调用该延时函数后,线程会被剥离CPU使用权,然后进入阻塞状态,直到延时结束,线程会重新获取CPU使用权才可继续运行,在线程阻塞的这段时间,CPU可以去执行其他的线程,如果其他的线程也在延时状态,那么CPU就将运行空闲线程。我们定义一个延时函数rt_thread_delay函数,其代码清单如下图:

      上面的代码中我们通过thread->remaining_tick来判断某个线程的延时是否结束,thread->remaining_tick是在SysTick_Handler中递减的,其代码如下:

void SysTick_Handler(void)
{
    /* 进入中断 */
    rt_interrupt_enter();

    rt_tick_increase();

    /* 离开中断 */
    rt_interrupt_leave();
}
/* 
 *    rt_interrupt_nest为中断计数器,是一个全局变量,用来记录中断嵌套次数。
 *    每进入一个中断函数,就会加一
 *    每离开一个中断函数,就会减一
 */
volatile rt_uint8_t rt_interrupt_nest;

/**
 * 当BSP文件的中断服务函数进入时会调用该函数
 * 
 * @note 请不要在应用程序中调用该函数
 *
 * @see rt_interrupt_leave
 */
void rt_interrupt_enter(void)
{
    rt_base_t level;
    
    
    /* 关中断 */
    level = rt_hw_interrupt_disable();
    
    /* 中断计数器++ */
    rt_interrupt_nest ++;
    
    /* 开中断 */
    rt_hw_interrupt_enable(level);
}


/**
 * 当BSP文件的中断服务函数离开时会调用该函数
 *
 * @note 请不要在应用程序中调用该函数
 *
 * @see rt_interrupt_enter
 */
void rt_interrupt_leave(void)
{
    rt_base_t level;
    
    
    /* 关中断 */
    level = rt_hw_interrupt_disable();
    
    /* 中断计数器-- */
    rt_interrupt_nest --;

    /* 开中断 */
    rt_hw_interrupt_enable(level);
}
//rt_tick 为系统时基计数器,是一个全局变量,用来记录产生了多少次SysTick中断
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 ++;

	/* 扫描就绪列表中所有线程的remaining_tick,如果不为0,则减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 --;
		}
	}
    
    /* 系统调度 */
	rt_schedule();
}

       下面我们来看一下main函数和线程1和线程2的函数体,在main函数中我们分别初始化了空闲线程、线程1和线程2并将它们插入到对应就绪列表的链表中去,然后就开始了系统调度(rt_system_scheduler_start),先从线程1开始执行(这里不理解的话,可以看另外一篇博客:<从0到1写RT-Thread内核——线程定义及切换的实现>),如果某个因为线程调用了rt_thread_delay函数而被阻塞了的话就运行另外的线程(此时线程阻塞剩余时长remaining_tick在SysTick_Handler中不断递减),如果非空闲线程都被阻塞了才运行空闲线程,如果某个线程的remaining_tick递减到为0了,则又继续运行该线程。

/************************************************************************
  * @brief  main函数
  * @param  无
  * @retval 无
  *
  * @attention
  *********************************************************************** 
  */
int main(void)
{	
	/* 硬件初始化 */
	/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
	
    /* 关中断,在程序开始的时候把中断关闭是一个好习惯,等系统初始化完毕,线程创建完毕,启动系统                                        
     * 调度的时候会重新打开中断(在rt_hw_context_switch_to函数中会再开启中断并设置中断标位)。
     * 如果一开始不关闭中断,接下来SysTick初始化完成,然后再初始化系统和创建线程,如果系统初始化
     * 和线程创建的时间大于SysTick中断周期的话,那么就会出现系统或者线程还没准备好的情况下就先执
     * 行了SysTick中断服务函数,在该函数中进行了系统调度,显示这是不合理的。
     */
    rt_hw_interrupt_disable();
    
    /* 初始化SysTick,调用固件库函数SysTick_Config来实现,
     * 配置中断周期为10ms(为100),中断优先级为最低
     */
    SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND );
	
	/* 调度器初始化 */
	rt_system_scheduler_init();

    /* 初始化空闲线程 */    
    rt_thread_idle_init();	
	
	/* 初始化线程1 */
	rt_thread_init( &rt_flag1_thread,                 /* 线程控制块 */
                    "rt_flag1_thread",                /* 线程名字,字符串形式 */
	                flag1_thread_entry,               /* 线程入口地址 */
	                RT_NULL,                          /* 线程形参 */
	                &rt_flag1_thread_stack[0],        /* 线程栈起始地址 */
	                sizeof(rt_flag1_thread_stack) );  /* 线程栈大小,单位为字节 */
	/* 将线程插入到就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
	
	/* 初始化线程2 */
	rt_thread_init( &rt_flag2_thread,                 /* 线程控制块 */
                    "rt_flag2_thread",                /* 线程名字,字符串形式 */
	                flag2_thread_entry,               /* 线程入口地址 */
	                RT_NULL,                          /* 线程形参 */
	                &rt_flag2_thread_stack[0],        /* 线程栈起始地址 */
	                sizeof(rt_flag2_thread_stack) );  /* 线程栈大小,单位为字节 */
	/* 将线程插入到就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );
	
	/* 启动系统调度器 */
	rt_system_scheduler_start(); 
}
/* 线程1 */
void flag1_thread_entry( void *p_arg )
{
	for( ;; )
	{
#if 0
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */		
		rt_schedule();
#else
        flag1 = 1;
        rt_thread_delay(2); 		
		flag1 = 0;
        rt_thread_delay(2);
#endif        
	}
}

/* 线程2 */
void flag2_thread_entry( void *p_arg )
{
	for( ;; )
	{
#if 0
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */
		rt_schedule();
#else
        flag2 = 1;
        rt_thread_delay(2); 		
		flag2 = 0;
        rt_thread_delay(2);
#endif        
	}
}

程序运行结果如下,其实这种阻塞延时的原理和我们在单片机裸机中采用的"前后台轮询"是非常相似的。

最后声明一下,我这里只是对学习的知识点进行总结,本文章的大多数知识来自于野火公司出版的《RT-Thread 内核实现与应用开发实战—基于STM32》,这本书非常不错,有志学习RT-Thread物联网操作系统的人可以考虑一下。

发布了42 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_37659294/article/details/100628547