• 韦东山老师 RTOS 入门课程(二)理解任务的创建,切换过程


    RTOS 的核心实现:保存,恢复现场

    接下来开始尝试实现 RTOS。当然我们开发的时候其实不用这样做,现在尝试实现只是为了更好地理解原理。

    RTOS 的核心就是刚才在研究的问题:保存和恢复现场。再追其本质,其实就是所有寄存器存入栈中了,以此保存。

    从上面的代码中可以发现,临时变量都是在每个过程自己的栈中的,而每一个过程都有自己的栈,这个相当于自动保存了。只要不去破坏,就不会收到影响(比如数组越界,可能就会影响到其他函数的栈)。

    1692447577446

    那么我们首先要实现一个创建任务的函数。创建任务要求如下:

    1. 任务保存现场,每个任务有自己独立的一套栈;
    2. 执行任务;
    3. 恢复现场。

    创建任务

    我们写一个创建任务函数,传入一个函数指针(这个函数是待执行的任务),传入这个函数执行需要的参数,传入一个栈数组来存取栈。这是一种伪造现场,就是实际上栈的实现是我们自己写程序写了一个数组来存,而不是硬件实现的。

    栈我们通过一个 1024 个字节的数组来伪造一个栈,可以存放 256 个寄存器数据(int 也是32位的,所以这里我们用 int 指针来表示栈指针)。

    数组自己是从低地址往高地址增长的,栈指针则相反,所以拿到数组基址后要先+1024 跳转到

    void create_task(task_function f, void *param, char *stack, int stack_len)
    {
    	int *top = stack + stack_len;//sp 指针,位于 stack[1024] 处,stack 是 char 类型数组
    	
    	/* 伪造现场 */
    	top -= 16;//要存16个数据,如下,就是r0-r12,lr,返回地址,xPSR
    	
    	/* r4~r11 */
    	top[0] = 0; /* r4 */
    	top[1] = 0; /* r5 */
    	top[2] = 0; /* r6 */
    	top[3] = 0; /* r7 */
    	top[4] = 0; /* r8 */
    	top[5] = 0; /* r9 */
    	top[6] = 0; /* r10 */
    	top[7] = 0; /* r11 */
    	
    	/* r0~r3 */
    	top[8]  = param; /* r0 */
    	top[9]  = 0; /* r1 */
    	top[10] = 0; /* r2 */
    	top[11] = 0; /* r3 */
    	
    	/* r12,lr */
    	top[12] = 0; /* r12 */
    	top[13] = 0; /* lr */
    	
    	/* 返回地址 */
    	top[14] = f; /* 任务入口 */
    	
    	/* PSR */
    	top[15] = (1<<24); /* psr 使用thumb指令集 */	
    	
    	/* 记录栈的位置 */
    	task_stacks[task_count++] = (int)top;//栈位置记录在这里,第 task_count 个数组元素的值就是指针指向的地址 int 值
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    psr 的值可以通过查表查看,比如规定是 thumb 还是 arm 的 isa 标志位。

    传入参数要注意,栈参数 stack 一定要各不相同,这样才能保证各个任务栈互不干扰,

    启动任务

    现在每个任务都可以有一个栈了。

    启动任务:把这个任务的栈中寄存器值,也就是上次停止运行时的状态恢复。

    我们可以在定时器中断中切换任务,这就是时间片轮询的实现。

    首先,这个任务想切换,得先创建好吧?没创建切换个锤。于是我们可以用一个变量 is_task_running 规定任务是否创建完成,定时器中断处理里如果发现没启动就先不做切换操作。

    然后,如果任务都创建完成了,就在定时器中断处理中修改当前任务的 flag 值来不断切换应该运行的任务。

    void SysTick_Handler(int lr_bak, int old_sp)
    {
    	int stack;
    	int pre_task;
    	int new_task;
    	
    	SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
    		
    	/* clear exception status */
    	SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
    
    	/* 如果还没有创建好任务, 直接返回 */
    	if (!is_task_running())
    	{
    		return;  // 表示无需切换
    	}
    	
    	/* 启动第1个任务或者切换任务 */
    	if (cur_task == -1)
    	{
    		/* 启动第1个任务 */
    		cur_task = 0;
    		
    		/* 从栈里恢复寄存器 */
    		/* 写汇编 */
    		stack = get_stack(cur_task);
    		StartTask_asm(stack, lr_bak);
    		
    		return ; /* 绝对不会运行到这里 */
    	}
    	else
    	{
    		/* 切换任务 */
    		// 取出下一个任务
    		pre_task = cur_task;
    		new_task = get_next_task();
    		
    		if (pre_task != new_task)
    		{			
    			/* 保存 pre_task: 在汇编里已经保存了 */
    			/* 更新sp */
    			set_task_stack(pre_task, old_sp);
    			
    			/* 切换 new_task */
    			stack = get_stack(new_task);
    			cur_task = new_task;
    			StartTask_asm(stack, lr_bak);
    		}
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    具体的恢复栈实现,我们需要操作汇编。我们查询栈数组找到当前应该恢复的栈的 int 值,这个值作为 sp,栈指针。

    StartTask_asm PROC
    				
    				; 从任务的栈里把R4~R11读出来写入寄存器
    				; r0 : 保存有任务的栈				
    				; r1 : 保存有LR(特殊值)
    				LDMIA r0!, { r4 - r11 }	;从 r0 开始读取一系列数据,边读边自增
    				
    				; 更新SP
    				MSR MSP, R0
    				;MOV SP, R0,因为栈操作只能用 msr
    				
    				; 触发硬件中断返回: 它会把栈里其他值读出来写入寄存器(R0,R1,R2,R3,R12,PSR)
    				BX R1
    				
    				ENDP
    				
    SysTick_Handler_asm PROC
    
    				; 在这里保存R4~R11
    				STMDB sp!, { r4 - r11 }
    				STMDB sp!, { lr }
    
    				MOV R0, LR ; LR是一个特殊值
    				ADD R1, SP, #4
    				BL SysTick_Handler  ; 这个C函数保证不破坏R4~R11
    				
    				LDMIA sp!, { r0 }
    				
    				LDMIA sp!, { r4 - r11 }
    				
    				BX R0
    				
    				ENDP
    
                     END
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    中断函数的返回地址是跳到一个处理函数,来处理剩余的硬件要处理的寄存器.

    发生中断 -> 系统自动跳到 SysTick_Handler_asm (当发生中断自动跳转的时候,此时 lr 是一个特殊值,是硬件恢复现场的地址。中断程序执行完毕返回的时候应当跳转到这里,触发硬件的恢复工作)-> 存储 R4-R11,lr -> 把 lr 和 sp+4 作为参数,开始调用 SysTick_Handler 函数 -> 获取新任务的栈空间 -> 调用 StartTask_asm,获取其数组栈中保存的数据 -> 跳转到 Task a,开始执行 -> 中断恢复。

    中断恢复:当跳转到 SysTick_Handler 函数时,此时 lr = 一个特殊值(如 0xFFFFFFF9),当程序跳转到这里的时候就会执行中断的返回,触发硬件的恢复工作。

    graph TB
    A[发生中断]-->B[硬件压栈,如 PC,LR,SP 等]
    B-->C[SysTick_Handler_asm]
    C-->D[SysTick_Handler]
    D-->E{当前是否有任务执行}
    E--是-->F[保存当前任务现场]
    F-->G
    E--否-->G[获取新任务栈]
    G-->H[StartTask_asm]
    H-->I[恢复 R4-R11]
    I-->J[跳转到硬件中断恢复现场处]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    main 创建的两个任务各自的栈是这样定义的:

    static char stack_a[1024] __attribute__ ((aligned (4)));;
    static char stack_b[1024] __attribute__ ((aligned (4)));;
    
    • 1
    • 2

    意为数组元素以4字节为长度对齐。因为字长32,存储寄存器状态也是4字节。

    切换任务

    保存A现场;取出B栈;切换任务。

    在启动任务的基础上加任务切换保存旧任务的代码。

    内容见上。

    补充内容

    1. 如果需要加数组的不同状态(锁定等),则可以定义一个状态数组,我们访问数组来判断当前这个任务能不能被切换。
    2. 同步互斥:为了进行原子操作,可以暂时关闭中断避免多个任务同时操作一个变量造成错误。
  • 相关阅读:
    vue2与vue3的使用区别与组件通信
    [ 云计算 华为云 ] 解决办法:如何更换华为云云耀云服务器L实例的镜像 | 文末送书
    STM32通用定时器产生PWM信号
    机器学习中的Bagging思想
    Python100天学习打卡第一天环境配置
    第2章:类加载子系统 详解
    FireFox火狐浏览器电脑端安装到D盘
    千万别再瞎学Python了,过来人的一些学习经验,能让你少走弯路
    linux安装MySql
    数据库总结
  • 原文地址:https://blog.csdn.net/jtwqwq/article/details/133633562