【STM32CubeIDE】STM32裸板多任务实现

前言

对于大部分的项目来讲,STM32基本都是处于“裸奔”的状态,将程序全部放在一个while(1)中顺序执行,但也会有部分任务要求几个任务“同时进行”,这个时候就需要一个RTOS了,但是有的时候就是这样,总会感觉为了一个小小的目的而在小容量的单片机中放一个OS,有些小题大做,我们需要的无非就是OS中的一个任务调度的功能,所以我只需要把这个功能给提取出来。

多任务

单片机只有一个处理核心,所以并不能同多核的PC机一样几个核心同时处理多个线程,我叫它“线程并行”。而单片机中所谓的多任务就是“线程并发”,就是多个线程看似一起发生,实际上还是单核心的顺序执行,只不过是每个线程都被分成了不同的时间片轮转执行。有关单片机多任务实现原理可以百度搜索一番,我在这里就不过多叭叭了,直接搞核心东西。

扩展知识

首先我们要知道,STM32有两个栈指针:MSP(主栈指针)和PSP(进程栈指针)有两种处理器模式:Thread ModeHandler Mode,有两种特权等级:特权模式非特权模式

STM32共有两个栈,主栈和进程栈,在裸机开发中全程使用主栈,而如果涉及到了多任务,就会用到进程栈,每个任务(其实就是函数)都会有一个属于任务本身的栈,这就是进程栈,这个栈的指针就要使用PSP,而这个时候到底是使用MSP还是PSP则是由 CONTROL 寄存器决定的, CONTROL[bit1] 若为0,则使用MSP,为 1 则使用PSP。中断或是异常的服务函数(例如 Systick_Handler 和 PendSV_Handler )一定是使用MSP(不需要人为控制,进入中断函数后 CONTROL[bit1] 位自动清零,函数退出后自动置 1 ),否则HardFault。

在 Thread Mode 中通过 CONTROL[bit0] 位可以选择处于特权模式或是非特权模式(0 特权模式,1 非特权模式),Handler Mode 一定是特权模式。(详情请自行搜索:任务运行在特权级或非特权级模式

功能实现

关于实现方式主要参考于《ARM Cortex-M3 Cortex-M4权威指南》第八章和第十章,主要用到了 PendSV 异常Systick 定时器中断,以及一些汇编语言的知识。

其中PendSV用于在PendSV_Handler中实现上下文切换(所谓上下文切换就是在多个线程之间反复横跳),Systick定时器则是将多个任务切分成时间片,每次进入 Systick_Handler 就会挂起一个PendSV异常,在 Systick_Handler以及其他高优先级异常任务处理完成之后 再进入PendSV_Handler。

在PendSV_Handler中其实只是做了两件工作:保存现场进程栈指针重定向。(这里需要用到汇编语言和寄存器的知识,如r0-r12,sp,lr,pc,msp,psp等,请自行搜索)

接下来分析代码。
这是使用STM32CubeIDE写的F407的代码,基于HAL库函数。

//宏定义 直接访问某个地址空间(1个字大小)
#define HW32_REG(ADDRESS) (*((volatile unsigned int *)(ADDRESS)))
//定义任务栈,每个2KB
long long task0Stack[256], task1Stack[256];

uint32_t currTask = 0;	//当前任务号
uint32_t nextTask = 1;	//下一个任务号
uint32_t PSP_Array[2];	//存放 进程栈指针 的数组

uint8_t initStatus = 0;	//初始化完成标志,
//HAL库的Systick初始化在HAL_Init()中就完成了,不想更改,只能加一个status

void Task0()
{
	initStatus = 1;	//进入Task0之后,任务调度开始
	while(1)
	{
		HAL_GPIO_TogglePin(LED_YELLOW_GPIO_Port, LED_YELLOW_Pin);
		HAL_Delay(300);
	}
}

void Task1()
{
	while(1)
	{
		HAL_GPIO_TogglePin(LED_RED_GPIO_Port, LED_RED_Pin);
		HAL_Delay(500);
	}
}

void TaskInit(void)
{
	//使能双字栈对齐
	SCB->CCR |= SCB_CCR_STKALIGN_Msk;

	//创建Task0的栈帧
[1] PSP_Array[0] = ((unsigned int) task0Stack) + (sizeof(task0Stack)) - 16 * 4;
	//栈帧指向Task0
	HW32_REG((PSP_Array[0] + (14 << 2))) = (uint32_t) Task0;
	//初始化xPSR
	HW32_REG((PSP_Array[0] + (15 << 2))) = 0x01000000;

	PSP_Array[1] = ((unsigned int) task1Stack) + (sizeof(task1Stack)) - 16 * 4;
	HW32_REG((PSP_Array[1] + (14 << 2))) = (uint32_t) Task1;
	HW32_REG((PSP_Array[1] + (15 << 2))) = 0x01000000;

	//使能PendSV异常
	HAL_NVIC_EnableIRQ(PendSV_IRQn);
	//设置PendSV为最低优先级,确保不会影响其他中断
	HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0);

	//设置psp指向Task0的栈顶
	asm volatile("ldr r1, =PSP_Array");
	asm volatile("msr psp, r1");

	//切换到进程栈,非特权模式
	asm volatile("mov r0, #0x3");
	asm volatile("msr control, r0");

	Task0();	//从Task0开始任务调度
}

//Systick定时器中断服务函数,1ms进入一次,挂起一次PendSV异常
void SysTick_Handler(void)
{
	HAL_IncTick();

	if (initStatus)
	{
		switch (currTask)
		{
		case 0:
			nextTask = 1;
			break;
		case 1:
			nextTask = 0;
			break;
		default:
			while (1)
				;
			break;
		}

		if (currTask != nextTask)
		{
			//挂起PendSV,等待高优先级任务结束后再执行PendSV_Handler
			SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
			asm volatile("isb");
		}
	}
}

void PendSV_Handler(void)
{
	//当前在PendSV_Handler中,所以用的是msp

	//恢复r7寄存器(这里是在填坑...)
[2]	asm volatile("pop    {r7}");

	//手动读取当前进程栈psp到r0
	asm volatile("mrs r0, psp");
	//将r4-r11这8个寄存器(存的是当前任务的相关信息)保存到当前进程栈(psp指向的)中
	asm volatile("stmdb r0!, {r4-r11}");

//保存当前上下文

	//获取当前任务号
	asm volatile("ldr r1, =currTask");
	asm volatile("ldr r2, [r1]");
	
	//获取 进程栈指针 的数组地址
	asm volatile("ldr r3, =PSP_Array");
	//将当前psp保存到 进程栈指针 数组
	asm volatile("str r0, [r3, r2, lsl #2]");

//加载下一个上下文
	//获取下一个任务号
	asm volatile("ldr r4, =nextTask");
	
	//设置 当前任务号 为 下一个任务号
	asm volatile("ldr r4, [r4]");
	asm volatile("str r4, [r1]");
	
	//从 任务栈指针 数组中加载下一个任务的psp
	asm volatile("ldr r0, [r3, r4, lsl #2]");
	//从下一个任务的进程栈中加载r4-r11(8个寄存器,存的是下一个任务的相关信息)
	asm volatile("ldmia r0!, {r4-r11}");
	//把psp指向下一个任务栈
	asm volatile("msr psp, r0");
	//返回(返回后会根据当前任务栈中保存的寄存器恢复现场,上次从哪里被打断的,这次就从哪里继续)
	asm volatile("bx lr");
}

关键点分析

在这里插入图片描述

栈帧:在函数调用过程中(在这里就是任务切换时),首先要做的就是保存现场,就是把相关的寄存器压栈,我们这里用的是进程栈,所以系统会在函数调用前自动的把部分寄存器和数值压栈,包括r0-r3r12lr返回地址xpsr,而这8个字就组成了一个栈帧,在函数调用结束后系统会自动从这个栈中弹出这个栈帧,是谓还原现场。至于双字栈对齐请自行百度。

[1]

	//创建Task0的栈帧
[1] PSP_Array[0] = ((unsigned int) task0Stack) + (sizeof(task0Stack)) - 16 * 4;
	//栈帧指向Task0
	HW32_REG((PSP_Array[0] + (14 << 2))) = (uint32_t) Task0;
	//初始化xPSR
	HW32_REG((PSP_Array[0] + (15 << 2))) = 0x01000000;

因为数组是向上增长,而栈是向下增长,所以进程栈的栈顶需设置为数组的尾部。而 task0Stack 是 long long 类型,8字节长度,PSP_Array是 unsigned int 类型,4字节长度,所以对于task0Stack来讲,一个8字的栈帧长度就是16*4,也就是Task0这个函数在task0Stack中的栈帧,第2行就是设置这个栈帧,第3行则是初始化xPSR,xPSR的bit9置1是说使用的是Thumb指令集,具体请百度。(以上是我根据实际情况做出的猜测,不知对错,希望有大佬能够指正)。

[2]

	//恢复r7寄存器(这里是在填坑...)
[2]	asm volatile("pop    {r7}");

开始的时候我并没有加这个指令,因为我根本没想到这个编译器会在进入函数前先“破坏现场”,如下图所示
在这里插入图片描述
在进入PendSV_Handler之后,执行指令之前,编译器先把 r7 压栈,然后把当前的栈指针sp存了进来,这就导致了本次保存任务现场stmdb r0!, {r4-r11}这句话就成了笑话,因为本次任务的现场指针sp事先就存到了r7寄存器中,记住是本次任务的,而进过编译器改变之后就变成了PendSV_Handler函数的现场指针了,所以下一次进入这个任务时,就会恢复现场失败,因为它根本找不到PendSV_Handler中的这个指针,直接HardFault。所以有了这句话pop {r7}

至此结束,写的不是很好,我也在学习,希望大佬们评论区多多指点讨论,尤其是[1]这一部分,不胜感激,谢谢!

  • 8
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值