在之前写的另外一篇文章——<从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 0flag1 = 1;delay( 100 ); flag1 = 0;delay( 100 );/* 线程切换,这里是手动切换 */ rt_schedule();
#elseflag1 = 1;rt_thread_delay(2); flag1 = 0;rt_thread_delay(2);
#endif }
}/* 线程2 */
void flag2_thread_entry( void *p_arg )
{for( ;; ){
#if 0flag2 = 1;delay( 100 ); flag2 = 0;delay( 100 );/* 线程切换,这里是手动切换 */rt_schedule();
#elseflag2 = 1;rt_thread_delay(2); flag2 = 0;rt_thread_delay(2);
#endif }
}
程序运行结果如下,其实这种阻塞延时的原理和我们在单片机裸机中采用的"前后台轮询"是非常相似的。
最后声明一下,我这里只是对学习的知识点进行总结,本文章的大多数知识来自于野火公司出版的《RT-Thread 内核实现与应用开发实战—基于STM32》,这本书非常不错,有志学习RT-Thread物联网操作系统的人可以考虑一下。