参考教程:【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili
一、中断的回顾与深入
1、概述
(1)让CPU打断正常运行的程序,转而去处理紧急的事件(程序),这个过程就叫中断,受理的事件称为中断服务程序。
(2)中断执行机制,可简单概括为三步:
①外设产生中断请求(GPIO外部中断、定时器中断等)。
②CPU停止执行当前程序,转而去执行中断处理程序(ISR)。
③执行完毕,返回被打断的程序处,继续往下执行。
2、中断优先级及分组
(1)ARM Cortex-M使用了8位宽的寄存器来配置中断的优先等级,这个寄存器就是中断优先级配置寄存器,但STM32只用了中断优先级配置寄存器的高4位[7 : 4],所以STM32提供了最大16级(0~15)的中断优先等级(中断优先级数值越小,等级越高)。
(2)STM32的中断优先级可以分为抢占优先级和子优先级:
①抢占优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断。
②子优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行。
(3)中断优先级分组一共有5种分配方式,对应着中断优先级分组的5个组,通过调用函数NVIC_PriorityGroupConfig(NVIC_PriorityGroup_x)可完成设置,建议将所有优先级位指定为抢占优先级位(即NVIC_PriorityGroup_4),方便µC/OS-III管理。
优先级分组 | 抢占优先级 | 子优先级 | 优先级配置寄存器高 4 位 |
NVIC_PriorityGroup_0 | 0 级抢占优先级 | 0-15 级子优先级 | 0bit 用于抢占优先级 |
NVIC_PriorityGroup_1 | 0-1 级抢占优先级 | 0-7 级子优先级 | 1bit 用于抢占优先级 |
NVIC_PriorityGroup_2 | 0-3 级抢占优先级 | 0-3 级子优先级 | 2bit 用于抢占优先级 |
NVIC_PriorityGroup_3 | 0-7 级抢占优先级 | 0-1 级子优先级 | 3bit 用于抢占优先级 |
NVIC_PriorityGroup_4 | 0-15 级抢占优先级 | 0 级子优先级 | 4bit 用于抢占优先级 |
(4)µC/OS-III的中断管理范围可通过宏CPU_CFG_KA_IPL_BOUNDA设置,如将该宏定义为4,即管理中断优先级范围为4~15。
(5)在中断服务函数中,如果调用到µC/OS-III的API函数,那么该中断优先级必须在µC/OS-III所管理的范围内。
3、中断相关寄存器
(1)STM32F103C8T6中有三个系统中断优先级配置寄存器,分别为SHPR1、SHPR2、SHPR3。
①SHPR1寄存器的地址为0xE000ED18,SHPR2寄存器的地址为0xE000ED1C,SHPR3寄存器的地址为0xE000ED20。
②PendSV中断设置最低优先级,这是为了保证系统任务切换不会阻塞系统其它中断的响应。
③SysTick中断设置µC/OS-III所管理的最高优先级,以保证系统时钟节拍的精度。
(2)STM32F103C8T6中有三个中断屏蔽寄存器,分别为PRIMASK、FAULTMASK和BASEPRI,µC/OS-III主要用到了BASEPRI和PRIMASK,其中中断管理就是利用BASEPRI这个寄存器,当需要关闭大部分中断时则使用PRIMASK这个寄存器。
①BASEPRI用于屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断,另外,进入临界区以及退出临界区,就是通过操作BASEPRI寄存器实现的。(BASEPRI寄存器也是低4位无效的,配置时需注意这一点)
②PRIMASK寄存器有32bit,但只有bit0有效,是可读可写的,将PRIMASK寄存器设置为1可屏蔽除NMI和HardFault外的所有异常和中断,将PRIMASK寄存器清0可使能中断。
二、µC/OS-III中断管理实验
1、原理图与实验目标
(1)原理图:
(2)实验目标:
①使用两个定时器TIM2和TIM3,TIM2中断的优先级为3,相应中断服务程序为控制LED1状态翻转,TIM3中断的优先级为6,相应中断服务程序为控制LED2状态翻转,系统所管理的优先级范围为4~15。
②设计2个任务——start_task、task3:
[1]start_task:用于初始化CPU库、配置Systick中断及优先级,并创建task3任务,然后删除自身。
[2]task3:中断测试任务,任务中将调用关中断和开中断函数来体现对中断的管理作用。
③预期实验现象:
[1]程序下载到板子上后,两个LED灯闪烁。
[2]按下按键1,LED2停止闪烁,LED1仍继续闪烁。
[3]按下按键2,LED2恢复闪烁。
2、实验步骤
(1)将“任务创建和删除实验”的工程文件夹复制一份,在拷贝版中进行实验。
(2)在stm32教程中找到“定时器定时中断”实验,复制其中的Timer.c文件和Timer.h文件,将其添加到本实验的项目中,如下图所示。
(3)将key.c文件以及UCOS_experiment.c文件中的阻塞函数更改为Delay_ms死延时函数,因为OSTimeDly函数的底层中调用了开中断函数,不利于实验现象的观察。
(4)在Timer.c文件中完成TIM2和TIM3初始化函数的实现,并在Timer.h文件中声明。
#include "stm32f10x.h" // Device header
void Timer2_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; //指定NVIC线路的抢占优先级为3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
void Timer3_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM3); //选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM3, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); //开启TIM3的更新中断
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //选择配置NVIC的TIM3线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6; //指定NVIC线路的抢占优先级为6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
}
(5)删除UCOS_experiment.c文件中关于task1和task2的内容,并更改task3函数的具体实现,按下按键1,µC/OS-III关中断,按下按键2,µC/OS-III开中断。
#include "stm32f10x.h" // Device header
#include "os.h"
#include "cpu.h"
#include "LED.h"
#include "Key.h"
#include <stdio.h>
#include "Delay.h"
/* START_TASK 任务 配置
* 包括: 任务优先级 任务栈大小 任务控制块 任务栈 任务函数
*/
#define START_TASK_PRIO 1
#define START_TASK_STACK_SIZE 256
CPU_STK start_task_stack[START_TASK_STACK_SIZE];
OS_TCB start_task_tcb;
void start_task(void);
/* TASK3 任务 配置
* 包括: 任务优先级 任务栈大小 任务控制块 任务栈 任务函数
*/
#define TASK3_PRIO 2
#define TASK3_STACK_SIZE 256
CPU_STK task3_stack[TASK3_STACK_SIZE];
OS_TCB task3_tcb;
void task3(void);
void UCOS_Test(void)
{
OS_ERR err;
OSInit(&err); //初始化μC/OS-III
//创建Start Task
OSTaskCreate (&start_task_tcb,
"start_task",
(OS_TASK_PTR)start_task,
NULL,
START_TASK_PRIO,
start_task_stack,
START_TASK_STACK_SIZE / 10,
START_TASK_STACK_SIZE,
0,
0,
0,
(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
&err);
/* 开始任务调度 */
OSStart(&err);
}
void start_task(void)
{
OS_ERR err; //接收错误代码使用,对错误代码进行分情况处理可增强程序鲁棒性
CPU_Init(); //初始化CPU库
CPU_SR_ALLOC();
//滴答定时器重装载值 = 系统主频 / 滴答定时器中断频率(滴答定时器是递减计数)
CPU_INT32U cnts = SystemCoreClock / OS_CFG_TICK_RATE_HZ;
OS_CPU_SysTickInit(cnts); //配置Systick中断及优先级
CPU_CRITICAL_ENTER(); //进入临界区(关中断)
//创建task3
OSTaskCreate (&task3_tcb,
"task3",
(OS_TASK_PTR)task3,
0,
TASK3_PRIO,
task3_stack,
TASK3_STACK_SIZE / 10,
TASK3_STACK_SIZE,
0,
0,
0,
(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
&err);
CPU_CRITICAL_EXIT(); //退出临界区(开中断)
OSTaskDel(NULL, &err); //删除任务自身
}
void task3(void)
{
uint8_t key = 0;
CPU_SR_ALLOC();
while(1)
{
key = Key_GetNum(); //读取按键键值
if(key == 1) CPU_CRITICAL_ENTER(); //关中断
if(key == 2) CPU_CRITICAL_EXIT(); //开中断
Delay_ms(10);
}
}
(6)main.c文件的主函数中需要配置NVIC中断优先级分组,然后初始化两个定时器(先分组,再配置,顺序不能反),并在主函数外实现两个定时器分别对应的中断服务程序,TIM2中断服务程序控制LED1状态翻转,TIM3中断服务程序控制LED2状态翻转。
#include "stm32f10x.h" // Device header
#include "LED.h"
#include "Key.h"
#include "UCOS_experiment.h"
#include "Timer.h"
int main(void)
{
/*模块初始化*/
Key_Init(); //Key初始化
LED_Init(); //LED初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //配置NVIC为分组4
//即抢占优先级范围:0~15,响应优先级范围:0
Timer2_Init(); //定时器2中断初始化
Timer3_Init(); //定时器3中断初始化
UCOS_Test();
while (1)
{
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
LED1_Turn(); //LED1状态翻转
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
LED2_Turn(); //LED2状态翻转
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
(7)程序完善好后点击“编译”,然后将程序下载到开发板上。
3、程序执行流程
(1)main函数全流程:
①初始化按键模块、LED模块,配置NVIC中断优先级分组,初始化定时器TIM2和TIM3。
②调用UCOS_Test函数(此处未画出中断可能将main函数打断)。
(2)测试函数全流程(此处未画出中断可能将测试函数打断):
①创建任务start_task。
②开启任务调度器。
(3)多任务调度执行阶段(发生在开启任务调度器以后):
①start_task任务函数首先进入临界区,在临界区中start_task任务不会被其它任务打断,接着start_task任务创建任务task3,然后删除自身,接着退出临界区,让出CPU资源。(此处未画出中断可能将start_task打断)
②start_task任务自我删除后,除了空闲任务以外,只剩下task3任务需要执行。
③按下按键1,中断优先级数值大于等于4的中断都被µC/OS-III关闭,即TIM3中断被关闭,但TIM2中断仍为打开状态。
④按下按键2,µC/OS-III打开所有能管理的中断,即TIM2中断和TIM3中断均为打开状态。