目录
3.2在FreeRTOS中配置PendSV和Systick中断优先级
1.什么是中断
简介:让CPU打断正常运行的程序,转而去处理紧急的事件(程序,ISR中断服务函数),就叫中断。
中断执行机制,可简单概括为三步:
1,中断请求:外设产生中断请求(GPIO外部中断、定时器中断等);
2,响应中断:CPU停止执行当前程序,转而去执行中断处理程序(ISR);
3,退出中断:执行完毕,返回被打断的程序处,继续往下执行。(打断哪个点,就返回哪个点继续执行)
举例说明:小明在看视频的时候突然肚子疼,必须要上厕所,打断看视频的任务,去上厕所, 暂停视频播放,上完厕所后,继续从暂停处继续观看视频。
2.中断优先级分组
2.1中断优先级分组-介绍
ARM Cortex-M 使用了 8 位宽的寄存器来配置中断的优先等级(最大,256个优先级,0~255),这个寄存器就是中断优先级配置寄存器。但STM32,只用了中断优先级配置寄存器的高4位 [7 : 4],所以STM32提供了最大16级的中断优先等级。
STM32 的中断优先级可以分为抢占优先级和子优先级
抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断
子优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行(例如有两个中断int1、int2,抢占优先级都为2,而子优先级int1为1,int2为0。假设int1正在执行,触发了int2,int2并不会抢占int1,因为抢占优先级相同,int2会在int1执行完后被执行。)
注意:中断优先级数值越小越优先
2.2中断优先级分组-配置
一共有 5 种分配方式,对应着中断优先级分组的 5 个组,FreeRTOS为了方便管理使用的是优先级分组4,全部用于抢占优先级。
优先级分组 | 抢占优先级 | 子优先级 | 优先级配置寄存器高 4 位 |
---|---|---|---|
NVIC_PriorityGroup_0 | 0 级抢占优先级 | 0-15 级子优先级 | 0bit 用于抢占优先级 4bit 用于子优先级 |
NVIC_PriorityGroup_1 | 0-1 级抢占优先级 | 0-7 级子优先级 | 1bit 用于抢占优先级 3bit 用于子优先级 |
NVIC_PriorityGroup_2 | 0-3 级抢占优先级 | 0-3 级子优先级 | 2bit 用于抢占优先级 2bit 用于子优先级 |
NVIC_PriorityGroup_3 | 0-7 级抢占优先级 | 0-1 级子优先级 | 3bit 用于抢占优先级 1bit 用于子优先级 |
NVIC_PriorityGroup_4 | 0-15 级抢占优先级 | 0 级子优先级 | 4bit 用于抢占优先级 0bit 用于子优先级 |
通过调用函数HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)即可完成设置(在HAL_Init中设置)
FreeRTOS官网关于中断说明:FreeRTOS官网中断说明地址
2.3中断优先级分组-特点
1、低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断里才允许调用FreeRTOS 的API函数(在代码中configMAX_SYSCALL_INTERRUPT_PRIORITY设置的是5,所以能被FreeRTOS操作的中断优先级是5~15)
2、建议将所有优先级位指定为抢占优先级位,方便FreeRTOS管理(调用函数HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4))
3、中断优先级数值越小越优先,任务优先级数值越大越优先
3.中断相关寄存器
3.1寄存器地址
三个系统中断优先级配置寄存器,分别为 SHPR1、 SHPR2、 SHPR3。
SHPR1寄存器首地址:0xE000ED18
SHPR2寄存器首地址:0xE000ED1C
SHPR3寄存器首地址:0xE000ED20
表中一个地址是8个位,一个寄存器是32位。如果想设置PendSV的优先级,则需要从SHPR3首地址偏移16位;如果想设置SysTick的优先级,则SHPR3首地址偏移24位。
注意:SysTick和PendSV的优先级设置,嘀嗒定时器Systick给系统提供心跳节拍,任务切换、调度都是在PendingSV中实现。
3.2在FreeRTOS中配置PendSV和Systick中断优先级
以下程序中SHPR3寄存器设置PendSV和SysTick的优先级。
其中oxe00ed20是寄存器SHPR3的首地址。
PendSV的首地址等于变量configKERNEL_INTERRUPT_PRIORITY左移16位,同理,Systick的首地址等于configKERNEL_INTERRUPT_PRIORITY左移24位。
configKERNEL_INTERRUPT_PRIORITY为变量configLIBRARY_LOWEST_INTERRUPT_PRIORITY左移8-configPRIO_BITS位(其中configPRIO_BITS宏定义为4,configLIBRARY_LOWEST_INTERRUPT_PRIORITY宏定义为15),即将15左移4位,STM32优先级配置低4位并没有用到,左移4位到高四位。
经过以上配置将PendSV和Systick设置优先级位15。
PendSV和SysTick设置最低优先级
设置最低:保证系统任务切换不会阻塞系统其他中断的响应(中断可以打断任务,任务不能打断中断,因为中断是较为紧急的事情)
3.3中断相关寄存器
三个中断屏蔽寄存器,分别为 PRIMASK、 FAULTMASK 和BASEPRI
FreeRTOS所使用的中断管理就是利用的BASEPRI这个寄存器。
BASEPRI:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断。
比如: BASEPRI设置为0x50,代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行。
关中断程序示例(中断优先级在5 ~ 15的全部被关闭):
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* FreeRTOS可管理的最高中断优先级 */
以上程序中变量 configMAX_SYSCALL_INTERRUPT_PRIORITY为0x50,赋值给变量ulNewBASEPRI ,使用汇编语言将ulNewBASEPRI 赋值给basepri寄存器。
当BASEPRI设置为0x50时:
在中断服务函数中调度FreeRTOS的API函数需注意:
1、中断服务函数的优先级需在FreeRTOS所管理的范围内;
2、在中断服务函数里边需调用FreeRTOS的API函数,必须使用带“FromISR”后缀的函数 ;
3、优先级分组必须设置为组4,全部设置成抢占优先级。
开中断程序示例(BASEPRI:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断):
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
以上程序将寄存器basepri设置成0。
4.FreeRTOS中断管理实验
1、实验目的:学会使用FreeRTOS的中断管理! 本实验会使用两个定时器,一个优先级为4,一个优先级为6,注意:系统所管理的优先级范围:5~15, 现象:两个定时器每1s,打印一段字符串,当关中断时,停止打印,开中断时持续打印。(优先级为6的定时器,在关闭中断时会停止打印,而优先级为4的定时器,不在5~15内,不受影响)
2、实验设计:将设计2个任务:start_task、task1
2个任务的功能如下:
start_task:用来创建task1任务;
task1:中断测试任务,任务中将调用关中断和开中断函数来体现对中断的管理作用。
本次实验基于本系列文章中【04】FreeRTOS的任务挂起与恢复 工程文件实现。
4.1修改freertos_demo.c
删除上一节中用到的任务2、任务3相关代码,并删除任务1中的数字打印和LED翻转,删除完如下所示:
#include "freertos_demo.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"
#include "key.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
/******************************************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1
#define START_TASK_STACK_SIZE 128
TaskHandle_t start_task_handler;
void start_task( void * pvParameters );
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handler;
void task1( void * pvParameters );
/******************************************************************************************************/
/**
* @brief FreeRTOS例程入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t ) start_task,
(char * ) "start_task",
(configSTACK_DEPTH_TYPE ) START_TASK_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t * ) &start_task_handler );
vTaskStartScheduler();
}
void start_task( void * pvParameters )
{
taskENTER_CRITICAL(); /*进入临界区*/
xTaskCreate((TaskFunction_t ) task1,
(char * ) "task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /*退出临界区*/
}
/* 任务一,实现每5000ms关闭和开启中断 */
void task1( void * pvParameters )
{
uint32_t task1_num=0;
while(1)
{
vTaskDelay(500);
}
}
4.2移除工程中exti.c文件
移除以下文件,并且在main.c文件中删除相关调用程序。
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "lcd.h"
#include "sdram.h"
#include "malloc.h"
#include "freertos_demo.h"
int main(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init();
KEY_Init();
freertos_demo();
}
4.3实现定时器功能
timer.c原始程序如下所示:
#include "timer.h"
#include "led.h"
TIM_HandleTypeDef TIM3_Handler; //定时器句柄
//通用定时器3中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!(定时器3挂在APB1上,时钟为HCLK/2)
void TIM3_Init(u16 arr,u16 psc)
{
TIM3_Handler.Instance=TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler=psc; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=arr; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
HAL_TIM_Base_Start_IT(&TIM3_Handler); //使能定时器3和定时器3更新中断:TIM_IT_UPDATE
}
//定时器底册驱动,开启时钟,设置中断优先级
//此函数会被HAL_TIM_Base_Init()函数调用
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
HAL_NVIC_SetPriority(TIM3_IRQn,1,3); //设置中断优先级,抢占优先级1,子优先级3
HAL_NVIC_EnableIRQ(TIM3_IRQn); //开启ITM3中断
}
}
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM3_Handler);
}
//回调函数,定时器中断服务函数调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==(&TIM3_Handler))
{
LED1=!LED1; //LED1反转
}
}
由于timer.c中已经实现了定时器3的程序,定时器3为通用定时器,此处选用TIM3和TIM4都为通用定时器(2022正点原子FreeRTOS教程中选用的是TIM6和TIM7基本定时器,此处有所不同)。timer.c修改完如下所示( 在中断回调函数中不建议使用printf()函数进行打印,耗时较长,此处为了方便演示):
#include "timer.h"
#include "led.h"
TIM_HandleTypeDef TIM3_Handler; //定时器3句柄
TIM_HandleTypeDef TIM4_Handler; //定时器4句柄
//通用定时器3,4中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!(定时器3挂在APB1上,时钟为HCLK/2)
void TIM3_Init(u16 arr,u16 psc)
{
TIM3_Handler.Instance=TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler=psc; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=arr; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
HAL_TIM_Base_Start_IT(&TIM3_Handler); //使能定时器3和定时器3更新中断:TIM_IT_UPDATE
}
void TIM4_Init(u16 arr,u16 psc)
{
TIM4_Handler.Instance=TIM4; //通用定时器4
TIM4_Handler.Init.Prescaler=psc; //分频系数
TIM4_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM4_Handler.Init.Period=arr; //自动装载值
TIM4_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM4_Handler);
HAL_TIM_Base_Start_IT(&TIM4_Handler); //使能定时器4和定时器4更新中断:TIM_IT_UPDATE
}
//定时器底册驱动,开启时钟,设置中断优先级
//此函数会被HAL_TIM_Base_Init()函数调用
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
HAL_NVIC_SetPriority(TIM3_IRQn,6,0); //设置中断优先级,抢占优先级6,子优先级0
HAL_NVIC_EnableIRQ(TIM3_IRQn); //开启ITM3中断
}
if (htim->Instance==TIM4)
{
__HAL_RCC_TIM4_CLK_ENABLE(); //使能TIM4时钟
HAL_NVIC_SetPriority(TIM4_IRQn,4,0); //设置中断优先级,抢占优先级4,子优先级0
HAL_NVIC_EnableIRQ(TIM4_IRQn); //开启ITM4中断
}
}
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM3_Handler);
}
//定时器4中断服务函数
void TIM4_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM4_Handler);
}
//回调函数,定时器中断服务函数调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==(&TIM3_Handler))
{
printf("TIM3优先级为6正在运行!\r\n");
}
else if (htim==(&TIM4_Handler))
{
printf("TIM4优先级为4正在运行!!!\r\n");
}
}
在timer.h中声明timer4的初始化函数,修改完如下所示:
#ifndef _TIMER_H
#define _TIMER_H
#include "sys.h"
extern TIM_HandleTypeDef TIM3_Handler; //定时器句柄
void TIM3_Init(u16 arr,u16 psc);
void TIM4_Init(u16 arr,u16 psc);
#endif
在main.c中调用TIM3和TIM4的初始化函数,并添加头文件,修改完如下所示:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "lcd.h"
#include "sdram.h"
#include "malloc.h"
#include "freertos_demo.h"
#include "timer.h"
int main(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init();
KEY_Init();
TIM3_Init(10000-1,9000-1); //定时器1s中断一次
TIM4_Init(10000-1,9000-1);
freertos_demo();
}
现象:
以上程序可以实现每隔1s,进入TIM3和TIM4,并打印。
4.4实现关闭和开启中断功能
在task1中关闭中断后延时5s使用的是死延时delay_ms(),并不使用vTaskDelay()函数,因为vTaskDelay()函数退出临界区时调用了开启中断函数portENABLE_INTERRUPTS(),因为刚刚关闭了中断,并不能调用开启中断函数。
freertos_demo.c部分程序修改如下(需要在一开始引用delay.h头文件):
#include "delay.h"
/* 任务一,实现每5000ms关闭和开启中断 */
void task1( void * pvParameters )
{
uint32_t task1_num=0;
while(1)
{
if(++task1_num==5)
{
task1_num=0;
printf("关中断!!\r\n");
portDISABLE_INTERRUPTS();
delay_ms(5000);
printf("开中断!!!\r\n");
portENABLE_INTERRUPTS();
}
vTaskDelay(1000);
}
}
现象:
在串口中每隔一秒进入TIM3和TIM4中断,进入5次,关闭中断;在串口中每隔1s进入TIM4中断, 开启中断;重复以上两步骤。