从0到1写RT-Thread内核——线程定义及切换的实现

从0写RT-Thread内核之线程定义及切换的实现具体可以分为以下六步来实现

一:分别定义线程栈、线程函数、线程控制块

ALIGN(RT_ALIGN_SIZE)//设置4字节对齐
/* 定义线程栈 */
rt_uint8_t rt_flag1_thread_stack[512];
rt_uint8_t rt_flag2_thread_stack[512];
/* 线程1 */
void flag1_thread_entry( void *p_arg )
{for( ;; ){flag1 = 1;delay( 100 );		flag1 = 0;delay( 100 );/* 线程切换,这里是手动切换 */		rt_schedule();//此函数在下面介绍}
}/* 线程2 */
void flag2_thread_entry( void *p_arg )
{for( ;; ){flag2 = 1;delay( 100 );		flag2 = 0;delay( 100 );/* 线程切换,这里是手动切换 */rt_schedule();//此函数在下面介绍}
}
/*        此结构体在rtdef.h里定义        */
struct rt_thread
{void        *sp;	          /* 线程栈指针 */void        *entry;	          /* 线程入口地址 */void        *parameter;	      /* 线程形参 */	void        *stack_addr;      /* 线程栈起始地址 */rt_uint32_t stack_size;       /* 线程栈大小,单位为字节 */rt_list_t   tlist;            /* 线程链表节点 */
};
typedef struct rt_thread *rt_thread_t;/*
*************************************************************************
*                               双向链表结构体
*************************************************************************
*/
struct rt_list_node
{struct rt_list_node *next;              /* 指向后一个节点 */struct rt_list_node *prev;              /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t;   /* 在main.c定义线程控制块 */
struct rt_thread rt_flag1_thread;
struct rt_thread rt_flag2_thread;

二:用线程初始化函数rt_thread_init(函数体如下图)调用rt_list_init函数初始化rt_list_t类型的节点,其实就是将节点里面的next和prev这两个节点指针指向节点本身,然后把上面第一步定义的线程栈、线程函数、线程控制块这三部分联系起来,实际上就是初始化线程控制块,由该函数的函数体我们知道它还调用了rt_hw_stack_init初始化了线程的栈

/* 初始化链表节点(在rtserver.h中定义) */
rt_inline void rt_list_init(rt_list_t *l)
{l->next = l->prev = l;
}

 

三:定义就绪列表并把线程插入到就绪列表中(线程控制块里有一个tlist成员,数据类型为rt_list_t,我们将线程插入就绪列表里面,就是通过将线程控制块的tlist这个节点插入到就绪列表中来实现的。如果把就绪列表比作晾衣杆,线程是衣服,那tlist就是晾衣架,每个线程都自带晾衣架,就是为了把自己挂在各种不同的链表中。)

/*        定义就绪列表        */
extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];就绪列表的下标对应的是线程的优先级,但是目前我们的线程还不支持优先级,有关支持多优先级的知识点我们后面会讲到,所以flag1和flag2线程在插入到就绪列表的时候,可以随便选择插入的位置。我们这里将flag1线程插入到就绪列表下表为0的链表中,flag2线程插入到就绪列表下标为1的链表中。/*        将线程1插入到就绪列表        */
rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );/*        将线程2插入到就绪列表        */
rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );

四:调度器的初始化(初始化就绪列表里面各个不同优先级的链表,其实就是将节点里面的next和prev这两个节点指针指向节点本身)

/* 初始化系统调度器 */
void rt_system_scheduler_init(void)
{	register rt_base_t offset;	/* 线程就绪列表初始化 */for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++){rt_list_init(&rt_thread_priority_table[offset]);}/* 初始化当前线程控制块指针 */rt_current_thread = RT_NULL;/* 初始化线程休眠列表,当线程创建好没有启动之前会被放入到这个列表 */rt_list_init(&rt_thread_defunct);
}

五:使用rt_system_scheduler_start()启动调度器,手动指定第一个运行的线程并切换到该线程中去(利用rt_hw_context_switch_to函数切换线程,该函数用于第一次线程的切换),该函数只是开启中断并设置中断标志位,真正实现线程切换是在中断服务函数里。

/* 启动系统调度器 */
void rt_system_scheduler_start(void)
{register struct rt_thread *to_thread;/* 手动指定第一个运行的线程 *//* rt_list_entry是一个已知一个结构体里面成员的地址,反推出该结构体的首地址的宏 *//* 此处是通过struct rt_thread里面tlist成员获取该结构体首地址,即获取该线程控制块的首地址 */to_thread = rt_list_entry(rt_thread_priority_table[0].next,struct rt_thread,tlist);rt_current_thread = to_thread;/* 切换到第一个线程,该函数在context_rvds.S中实现,在rthw.h声明,用于实现第一次任务切换。当一个汇编函数在C文件中调用的时候,如果有形参,则执行的时候会将形参传人到CPU寄存器r0。*/rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);
}

六:定义一个void rt_schedule(void)函数,并在其中调用rt_hw_context_switch()函数(该函数实现新老线程的切换,和第五点中的rt_hw_context_switch_to()一样,都只是开启中断并设置中断标志位,真正的线程切换在中断服务函数中实现)

/* 系统调度 */
void rt_schedule(void)
{struct rt_thread *to_thread;struct rt_thread *from_thread;/* 两个线程轮流切换 */if( rt_current_thread == rt_list_entry( rt_thread_priority_table[0].next,struct rt_thread,tlist) ){from_thread = rt_current_thread;to_thread = rt_list_entry( rt_thread_priority_table[1].next,struct rt_thread,tlist);rt_current_thread = to_thread;}else{from_thread = rt_current_thread;to_thread = rt_list_entry( rt_thread_priority_table[0].next,struct rt_thread,tlist);rt_current_thread = to_thread;																		 }/* 产生上下文切换 */rt_hw_context_switch((rt_uint32_t)&from_thread->sp,(rt_uint32_t)&to_thread->sp);	}

中断服务函数PendSV_Handler的实现如下,该函数真正的实现了线程的切换

;/*
; *-----------------------------------------------------------------------
; * void PendSV_Handler(void);
; * r0 --> switch from thread stack
; * r1 --> switch to thread stack
; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
; *-----------------------------------------------------------------------
; */PendSV_Handler   PROCEXPORT PendSV_Handler; 失能中断,为了保护上下文切换不被中断MRS     r2, PRIMASKCPSID   I; 获取中断标志位,看看是否为0LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit; r1不为0则清0MOV     r1, #0x00STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0; 判断rt_interrupt_from_thread的值是否为0LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1CBZ     r1, switch_to_thread                     ; 判断r1是否为0,为0则跳转到switch_to_thread; 第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread; ========================== 上文保存 ==============================; 当进入PendSVC Handler时,上一个线程运行的环境即:; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参); 这些CPU寄存器的值会自动保存到线程的栈中,剩下的r4~r11需要手动保存MRS     r1, psp                                  ; 获取线程栈指针到r1STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_threadSTR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp; ========================== 下文切换 ==============================
switch_to_threadLDR     r1, =rt_interrupt_to_thread               ; 加载rt_interrupt_to_thread的地址到r1; rt_interrupt_to_thread是一个全局变量,里面存的是线程栈指针SP的指针LDR     r1, [r1]                                  ; 加载rt_interrupt_to_thread的值到r1,即sp指针的指针LDR     r1, [r1]                                  ; 加载rt_interrupt_to_thread的值到r1,即spLDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11MSR     psp, r1                                   ;将线程栈指针更新到PSPpendsv_exit; 恢复中断MSR     PRIMASK, r2ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参); 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。; PendSV_Handler 子程序结束ENDP	ALIGN   4END

     通过上面六个步骤,我们已经完成了线程切换的各个函数,接下来的是我们main()函数,先初始化调度器,然后分别初始化线程1和2并把两者分别插入到就绪列表中的0号和1号链表,然后启动系统调度器,启动系统调度器之后就会进行线程的第一次切换,切换到线程flag1的函数中去,在该函数的最后又会切换到线程flag2中去,然后线程flag2又会切换会线程flag1,一直这样来回切换。

/************************************************************************* @brief  main函数* @param  无* @retval 无** @attention*********************************************************************** */
int main(void)
{	/* 硬件初始化 *//* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */	/* 调度器初始化 */rt_system_scheduler_init();/* 初始化线程 */rt_thread_init( &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) );/* 初始化线程 */rt_thread_init( &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(); 
}

      最后我们通过软件仿真的效果如下图,可以看到flag1和flag2两个变量波形的占空比为1/4,刚好符合我们两个线程函数来回切换的效果。

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

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

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

相关文章

从0到1写RT-Thread内核——临界段的保护

临界段就是一段在执行的时候不能被中断的代码段&#xff0c;在RT-Thread里&#xff0c;临界段最常出现的就是对全局变量的操作&#xff08;类似Linux下的锁&#xff09;。RT-Thread对临界段的保护是直接把中断全部关了&#xff0c;NMI FAULT和硬FAULT除外。下图是3个关于中断屏…

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

在之前写的另外一篇文章——<从0到1写RT-Thread内核——线程定义及切换的实现>中线程体内的延时使用的是软件延时&#xff0c;即还是让CPU空等来达到延时的效果。RTOS中的延时叫阻塞延时&#xff0c;即线程需要延时的时候&#xff0c;线程会放弃CPU的使用权&#xff0c;C…

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

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

Linux串口阻塞与非阻塞

Linux串口编程的阻塞与否可以在open函数中设置&#xff0c;例如&#xff1a; 打开时使用&#xff1a; fd open(USAR1, O_RDWR | O_NOCTTY );//阻塞式读写fd open("/dev/ttyAT2",O_RDWR|O_NOCTTY|O_NDELAY); //非阻塞读写 除了用open函数之外还可以在open函数之…

vi中如何实现批量替换

将文件tihuan&#xff08;假设此文本中字符a&#xff09;中的所有字符a换成字符w&#xff0c;其命令为&#xff1a; 1。vi tihuan 2。按esc键 3。按shift&#xff1a; 4。在&#xff1a;后输入 %s/a/w/g 其中s为&#xff1a;substitute&#xff0c;%表示所有行&#xff0c;…

C++里数组名+1和数组名的地址+1的区别

C/C里面的数组名字会退化为指针&#xff0c;所以数组名a实际指的是数组的第一个元素的地址。而数组名作为指针来讲有特殊性&#xff0c;它正在它所指向的内存区域中&#xff0c;&a的值和a的数值是相同的&#xff08;可以输出观察一下&#xff09;&#xff0c;但是类型和意义…

栈空间和堆空间的区别

栈空间用于存储函数参数和局部变量&#xff0c;所需空间由系统自动分配&#xff0c;回收也由系统管理&#xff0c;无需人工干预&#xff1b;堆空间用于存储动态分配的内存块&#xff0c;分配和释放空间均由程序员控制&#xff0c;有可能产生内存泄漏。 栈空间作为一个严格后进…

AD软件之模块化原理图

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

进程与线程的区别(面试题)

进程与线程的区别 1.进程是资源分配最小单位&#xff0c;线程是程序执行的最小单位&#xff1b; 2..进程有自己独立的地址空间&#xff0c;每启动一个进程&#xff0c;系统都会为其分配地址空间&#xff0c;建立数据表来维护代码段、堆栈段和数据段&#xff0c;线程没有独立的…

查找表的原理与结构 什么是竞争与冒险现象?怎样判断?如何消除?

查找表的原理与结构&#xff1a; 查找表&#xff08;look-up-table&#xff09;简称为LUT&#xff0c;LUT本质上就是一个RAM。目前FPGA中多使用4输入的LUT&#xff0c;所以每一个LUT可以看成一个有 4位地址线的16x1的RAM。当用户通过原理图或HDL语言描述了一个逻辑电路以…

AD软件操作技巧

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

冒泡排序算法,C语言冒泡排序算法详解

冒泡排序是最简单的排序方法&#xff0c;理解起来容易。虽然它的计算步骤比较多&#xff0c;不是最快的&#xff0c;但它是最基本的&#xff0c;初学者一定要掌握。 冒泡排序的原理是&#xff1a;从左到右&#xff0c;相邻元素进行比较。每次比较一轮&#xff0c;就会找到序列中…

白话经典算法系列之六 快速排序 快速搞定

快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高&#xff0c;因此经常被采用&#xff0c;再加上快速排序思想----分治法也确实实用&#xff0c;因此很多软件公司的笔试面试&#xff0c;包括像腾讯&#xff0c;微软等知名IT公司都喜欢考这个&#xff0c;还有大大小…

c语言中判断一个字符串是否包含另一个字符串

1. 使用库函数 string.h strstr函数 函数名: strstr 功 能: 在串中查找指定字符串的第一次出现 用 法: char *strstr(char *str1, char *str2); 说明&#xff1a;返回指向第一次出现str2位置的指针&#xff0c;如果没找到则返回NULL。 调用函数,判断返回值是否等于NULL…

C语言截取从某位置开始指定长度子字符串方法

c语言标准库没有截取部分字符串的函数&#xff0c;为啥&#xff1f;因为用现有函数strncpy&#xff0c;很容易做到&#xff01; char dest[4] {""}; char src[] {"123456789"}; strncpy(dest, src, 3); puts(dest); 输出结果为 123 看到了吗&#xff…

Modbus通讯协议详细解释

https://blog.csdn.net/rxiang12/article/details/79125813

V4L2框架分析

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

Linux下IO多路复用之select函数的使用

select函数的作用&#xff1a; 如果我们的程序里有两个需要阻塞的地方&#xff0c;例如要从服务器读数据&#xff0c;同时还要从键盘上读数据&#xff08;若不采用阻塞而用查询的方式则大量占用系统资源&#xff09;。这个时候我们就有两处阻塞&#xff0c;你当然可以用多线程或…

条件变量实现线程同步

(1) 什么是条件变量实现线程同步?   假如我们的程序中有两个线程&#xff0c;一个是生产者线程&#xff0c;另一个是消费者线程&#xff0c;生产者线程每隔一段时间把数据写入到缓冲区buffer中&#xff0c;而消费者线程则每隔一段时间从buffer中取出数据&#xff0c;为了避免…

mjpg-streamer框架分析

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