从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 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物联网操作系统的人可以考虑一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384522.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从0到1写RT-Thread内核——支持多优先级

在本章之前&#xff0c;RT-Thread还没有支持多优先级&#xff0c;我们手动指定了第一个运行的线程&#xff0c;并在此之后三个线程&#xff08;包括空闲线程&#xff09;互相切换&#xff0c;在本章中我们加入优先级的功能&#xff0c;第一个运行的程序是就绪列表里优先级最高的…

AD软件之模块化原理图

首先我们创建两个原理图文件 然后我们在Sheet2.SchDoc里放置一个页面符并双击绿色的方框 选择目标文件 我们选择我们刚才创建的Sheet4.SchDoc 然后在 视图——>面板——>Navigator选项 里点一下交互式导航 就可以看到Sheet4.SchDoc被添加到Sheet2.SchDoc下面了 通过上面…

AD软件操作技巧

本文介绍一些关于AD软件的实用小操作&#xff0c;这些小技巧可以大大的减少我们的工作量 一.批量操作丝印&#xff08;或者操作别的东西也可以&#xff0c;主要是凸显批量操作的思想&#xff09; 如下图假设我们工程里有很多丝印和焊盘等等&#xff0c;现在我想改批量地修改丝…

V4L2框架分析

V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。v4L2是针对uvc&#xff08;USB Video Class&#xff09;免驱usb设备的编程框架&#xff0c;主要用于采集usb摄像头等。 下图是V4L2的框架&#xff0c;首先系统核心层分配设置注册一个名为cdev结构体变量&#x…

mjpg-streamer框架分析

mjpg-streamer程框架图如下所示&#xff1a; 程序运行起来后&#xff0c;主进程根据传入的参数设置的输入输出通道打开对应的输入输出动态链接库&#xff0c;并依次调用以下函数 1、输入---仓库-----输出&#xff08;mjpg-streamer.h&#xff09; &#xff08;1&#xff09;gl…

linux字符驱动之概念介绍

一、字符驱动框架 问&#xff1a;应用程序open、read、write如何找到驱动程序的open、read、write函数&#xff1f; 答:应用程序的open、read、write是在C库里面实现的&#xff0c;它里面通过swi val指令去触发一个异常&#xff0c;这个异常就会进入到内核空间&#xff0c;在内…

USB摄像头视频监控项目学习笔记

一个摄像头监控应用程序的系统调用如下所示&#xff1a; /* open * VIDIOC_QUERYCAP 确定它是否视频捕捉设备,支持哪种接口(streaming/read,write) * VIDIOC_ENUM_FMT 查询支持哪种格式 * VIDIOC_S_FMT 设置摄像头使用哪种格式 * VIDIOC_REQBUFS 申请buffer 对于 str…

图片缩放算法

项目背景&#xff1a;博主之前做过一个摄像头采集数据&#xff0c;然后在LCD上显示视频数据的项目&#xff0c;假如我们摄像头采集的一帧数据的分辨率比我们的LCD的分辨率要大&#xff0c;那么LCD则无法显示整个图像&#xff0c;这时候我们就要把这么一帧图片进行缩放&#xff…

数码相框项目之显示一张可放大、缩小、拖拽的图片

之前我做过一个电子相框的项目&#xff0c;涉及到的重难点主要为&#xff1a;在LCD上放大、缩小、移动图片。 首先我们得明白的一点是&#xff1a;无论是放大或缩小&#xff0c;实际上都是对原图进行等比例的缩小&#xff0c;然后在LCD上面显示&#xff0c;只不过缩小的程度不…

TCP协议-如何保证传输可靠性

TCP协议传输的特点主要就是面向字节流、传输可靠、面向连接。这篇博客&#xff0c;我们就重点讨论一下TCP协议如何确保传输的可靠性的。 确保传输可靠性的方式 TCP协议保证数据传输可靠性的方式主要有&#xff1a; 校验和序列号确认应答超时重传连接管理流量控制拥塞控制 校…

TCP协议-握手与挥手

认识TCP协议 TCP全称为“传输控制协议”&#xff0c;这是传输层的一个协议&#xff0c;对数据的传输进行一个详细的控制。 特点&#xff1a; 面向字节流安全可靠面向连接 TCP协议段格式 源端口号与目的端口号&#xff1a;这里与UDP的一样&#xff0c;每个数据都要知道从哪个…

ASOC注册过程

一、什么是ASOC 在嵌入式系统里面的声卡驱动为ASOC&#xff08;ALSA System on Chip&#xff09; &#xff0c;它是在ALSA 驱动程序上封装的一层&#xff0c;分为3大部分&#xff0c;Machine&#xff0c;Platform和Codec ,三部分的关系如下图所示&#xff1a;其中Machine是指我…

ASOC调用过程

上一篇文章我们将了嵌入式系统注册声卡的过程&#xff1a;https://blog.csdn.net/qq_37659294/article/details/104748747 这篇文章我们以打开一个声卡的播放节点为例&#xff0c;讲解一下在APP调用open时&#xff0c;最终会如何调用到硬件相关的函数。 在上一篇文章最后我们说…

进程上下文与中断上下文的理解

一.什么是内核态和用户态 内核态&#xff1a;在内核空间执行&#xff0c;通常是驱动程序&#xff0c;中断相关程序&#xff0c;内核调度程序&#xff0c;内存管理及其操作程序。 用户态&#xff1a;用户程序运行空间。 二.什么是进程上下文与中断上下文 1.进程上下文&#xf…

内核的Makefile与Kconfig关系解析

在子目录下的Kconfig里添加make menuconfig的选项&#xff08;如图一&#xff09;&#xff0c;并默认设置为y&#xff0c;make menuconfig的菜单里就会有该项并默认为选上状态&#xff0c;make menuconfig配置完之后在.config文件里就有该选项&#xff0c;并等于y&#xff08;如…

Linux信号之signal函数

1. 信号概述 何为信号&#xff1a;信号就是由用户、系统或进程发送给目标进程的信息&#xff0c;以通知目标进程中某个状态的改变或是异常。 信号产生&#xff1a;总体来说&#xff0c;其产生的条件有两种&#xff0c;分别是&#xff1a;硬件和软件原因&#xff0c;又称为&…

Linux中wait()函数及waitpid()函数

编程过程中&#xff0c;有时需要让一个进程等待另一个进程&#xff0c;最常见的是父进程等待自己的子进程&#xff0c;或者父进程回收自己的子进程资源包括僵尸进程。这里简单介绍一下系统调用函数&#xff1a;wait() 函数原型是 #include <sys/types.h> #include <…

学习笔记 --- DM9000网卡原理与基地址设置

前面有文章分析了网卡也是属于类内存总线的设备&#xff0c;类内存总线的设备有地址总线和数据总线&#xff0c;先来看下DM9000的管脚&#xff1a; 从上面可以看出DM9000的地址总线就一根&#xff0c;它不像CS8900那样地址总线和数据总线都齐全。而这里只有一根地址线(CMD)&…

静态VLAN的配置

在一台交换机上连接3台PC机&#xff0c;然后创建两个VLAN&#xff0c;分别为VLAN 10 和VLAN 20&#xff0c;把第一台PC机分配给VLAN 10&#xff0c;把其他两台分配给VLAN 20.然后测试他们的互通情况。 在这里命令我用的都是简化命令&#xff0c;想卡完整版命令&#xff0c;请到…

静态路由原理

1、路由器的工作原理 路由工作简单原理图 1&#xff09;主机1.1要发生数据包给主机4.1.因为IP地址不在同一网段&#xff0c;所以主机会将数据包发送给本网段的网关路由器。 2&#xff09;路由器A 接收到数据包&#xff0c;先查看数据包IP首部中的目标IP地址。再查找自己的路由表…