目录
UCOSIII实现LED0、LED1灯点亮、按键控制led灯
NVIC_PriorityGroupConfig() 优先级分组配置函数。
UCOSIII实现LED0、LED1灯点亮、按键控制led灯
开始分区对代码进行解读前请先概览一下总体代码;主要实现灯0和灯1闪烁,按下按键0后灯0停止闪烁
本文将从头文件开始一行行进行代码和相应api函数进行讲解,下面先展示代码源码和实验结果图:
注:本次实验用的开发板为正点原子的探索板
5280125
一、头文件
想了解下面这些头文件具体是干嘛的,请关注后续会详细进行介绍,目前只需对此有个大体了解
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "includes.h"
#include "stm32f4xx_rcc.h"
二、创建开始函数创建任务前的准备工作和创建任务函数
2.1 创建任务函数OSTaskCreate介绍
//创建任务函数
void OSTaskCreate (OS_TCB *p_tcb, //*p_tcb: 指向任务的任务控制块OS_TCB。
CPU_CHAR *p_name, //*p_name: 指向任务的名字,我们可以给每个任务取一个名字
OS_TASK_PTR p_task, //p_task: 执行任务代码,也就是任务函数名字通常习惯和任务函数起一样的名字)
void *p_arg, //*p_arg: 传递给任务的参数
OS_PRIO prio, // 任务优先级,数值越低优先级越高,用户不能使用系统任务使用的那些优先 级!
CPU_STK *p_stk_base, //*p_stk_base:指向任务堆栈的基地址。
CPU_STK_SIZE stk_limit, //stk_limit: 任务堆栈的堆栈深度,用来检测和确保堆栈不溢出。
CPU_STK_SIZE stk_size, //stk_size: 任务堆栈大小
OS_MSG_QTY q_size, //q_size: 选的内部消息队列,通常不使用该功能,设为0
OS_TICK time_quanta,//使能时间片轮转调度时用来设置任务的时间片长度,默认值为时钟节拍/10。
void *p_ext, //*p_ext: 指向用户补充的存储区。
OS_OPT opt,
OS_ERR *p_err) //用来保存调用该函数后返回的错误码。
-
q_size: UCOSIII 中每个任务都有一个可选的内部消息队列,我们要定义宏 OS_CFG_TASK_Q_EN>0,这是才会使用这个内部消息队列。
-
opt: 包含任务的特定选项,有如下选项可以设置。 OS_OPT_TASK_NONE 表示没有任何选项 OS_OPT_TASK_STK_CHK 指定是否允许检测该任务的堆栈 OS_OPT_TASK_STK_CLR 指定是否清除该任务的堆栈 OS_OPT_TASK_SAVE_FP 指定是否存储浮点寄存器,CPU 需要有浮点 运算硬件并且有专用代码保存浮点寄存器。
-
从上面创建开始任务可以看出,想创建一个任务,我们起码需要:控制块、任务函数声明(先声明后定义)、函数优先级(通常宏定义)、任务栈(可以用数组的方式创建,也可以用申请内存的方式创建),以一个起始任务为例:
//start_task
#define START_TASK_PRIO 3
#define star_task_size 256
CPU_STK start_task_stk[star_task_size]; //重点讲解此处
OS_TCB tcb; 重点讲解此处
void start_task (void *p_arg);
然后根据start_task为模板,分别创建任务1、任务2、任务3,具体参见总代码这一节,但是需要注意各个任务的优先级以及内存大小
2.2 CPU_STK数据类型
CPU_STK其实是就是一个数据类型unsigned int,见源码
typedef unsigned int CPU_INT32U //unsigned int占4个字节 32位
typedef CPU_INT32U CPU_STK;
2.3 OS_TCB结构体数据类型
OS_TCB是一个结构体,上述代码的tcb其实就是创建的一个结构体
typedef struct os_tcb OS_TCB;
struct os_tcb {
CPU_STK *StkPtr;
void *ExtPtr;
CPU_STK *StkLimitPtr;
OS_TCB *NextPtr;
OS_TCB *PrevPtr;
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr;
CPU_CHAR *NamePtr;
CPU_STK *StkBasePtr;}
三、主函数讲解
3.1 外设初始化、os初始化和中断状态设置等
int main(void)
{
OS_ERR err;
CPU_SR_ALLOC();
delay_init(168); //时钟初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组配置
uart_init(115200); //串口初始化
INTX_DISABLE(); //关中断,防止滴答定时器对外设初始化的打扰
LED_Init(); //LED 初始化
KEY_Init();
INTX_ENABLE(); //开中断
OSInit(&err); //初始化UCOSIII
OS_CRITICAL_ENTER();
CPU_SR_ALLOC()、OS_CRITICAL_ENTER()、CPU_CRITICAL_EXIT()、NVIC_PriorityGroupConfig()
CPU_SR_ALLOC()
是 uC/OS-III 中的一个宏定义。它用于声明和定义一个用于保存中断状态的变量。
在 uC/OS-III 中,由于任务切换和中断处理需要保存和恢复中断状态,通常使用一个变量来保存当前的中断状态。CPU_SR_ALLOC()
宏就是用来声明这样一个变量的。具体而言,CPU_SR_ALLOC()
宏会根据所使用的编译器和目标平台的不同,生成相应的代码,以定义一个用于保存中断状态的变量。这个变量的类型通常是 CPU 的状态寄存器的数据类型。
示例用法如下:
CPU_SR cpu_sr;
CPU_SR_ALLOC();
// 在需要保存中断状态的地方
CPU_CRITICAL_ENTER(); // 保存中断状态// 禁用中断并保存中断状态,进入临界区,临界区后续有讲解
// 执行一些关键操作
CPU_CRITICAL_EXIT(); // 恢复中断状态
通过使用 CPU_SR_ALLOC()
宏和相应的 CPU_CRITICAL_ENTER()
和 CPU_CRITICAL_EXIT()
宏,您可以在 uC/OS-III 中正确地保存和恢复中断状态,以确保系统在关键代码段中的可靠性和正确性。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)
是针对 ARM Cortex-M 处理器的 NVIC(Nested Vectored Interrupt Controller)优先级分组配置函数。在 ARM Cortex-M 处理器中,NVIC 是用于管理中断的控制器。中断是一种事件,可以中断正在执行的程序并立即处理。NVIC 通过控制中断优先级来确保在同时发生多个中断时的正确处理顺序。
3.2 创建开始任务
cOSTaskCreate( (OS_TCB*) &tcb,
(CPU_CHAR*) "start_task",
(OS_TASK_PTR) start_task,
(void*) 0,
(OS_PRIO) START_TASK_PRIO,
(CPU_STK*) &start_task_stk[0],
(CPU_STK_SIZE) star_task_size/10,
(CPU_STK_SIZE) star_task_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OS_CRITICAL_EXIT(); //
OSStart(&err);
while(1);
}
为什么要先禁用状态,再创建任务,然后恢复中断退出临界区?
在给出的代码片段中,确实是在禁用中断进入临界区后创建开始任务。
是为了确保在创建开始任务期间不会发生任务切换或中断干扰,从而保证开始任务的创建过程是原子性的。
如果没有进入临界区禁用中断,其他任务或中断可能会在这个过程中被调度执行,导致不一致或不可预测的结果。
在禁用中断期间,系统的实时性和响应性可能会受到影响,因为其他中断和任务将无法得到处理。因此,在确定使用临界区禁用中断的情况下,应该仔细权衡系统的需求和设计,确保在合适的时机使用临界区,以最小化对系统性能的影响。
任务创建好后,该任务处于什么状态?
它会进入就绪状态,并等待调度器的调度。在合适的时机,调度器会选择该任务作为下一个要执行的任务,并将其切换到运行状态(Running State)。虽然任务处于就绪状态,但并不意味着它会立即执行。任务的执行顺序和调度是由任务调度器决定的,它根据任务的优先级、调度策略和系统资源等因素进行决策。
3.3 定义开始任务
void start_task (void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); //
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
#endif
OS_CRITICAL_ENTER();
OSTaskCreate( (OS_TCB*) &tcb1,
(CPU_CHAR*) "task1",
(OS_TASK_PTR) task1,
(void*) 0,
(OS_PRIO) TASK1_PRIO,
(CPU_STK*) &task1_stack[0],
(CPU_STK_SIZE) task1_size/10,
(CPU_STK_SIZE) task1_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OSTaskCreate( (OS_TCB*) &tcb2,
(CPU_CHAR*) "task2",
(OS_TASK_PTR) task2,
(void*) 0,
(OS_PRIO) TASK2_PRIO,
(CPU_STK*) &task2_stack[0],
(CPU_STK_SIZE) task2_size/10,
(CPU_STK_SIZE) task2_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OSTaskCreate( (OS_TCB*) &tcb3,
(CPU_CHAR*) "task3",
(OS_TASK_PTR) task3,
(void*) 0,
(OS_PRIO) TASK3_PRIO,
(CPU_STK*) &task3_stack[0],
(CPU_STK_SIZE) task3_size/10,
(CPU_STK_SIZE) task3_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OS_TaskSuspend((OS_TCB*) &tcb,&err);
OS_CRITICAL_EXIT();
}
任务函数中常见的设置
在任务函数中,以下是一些常见的操作和设置:
-
OS_ERR err;
:定义一个变量err
,用于存储函数调用的返回错误码。 -
CPU_SR_ALLOC();
:定义一个变量CPU_SR
,用于保存中断状态的信息。 -
p_arg = p_arg;
:将函数的参数p_arg
赋值给自身,这个操作可能是为了避免编译器警告未使用的变量。 -
CPU_Init();
:初始化 CPU,根据代码中的调用,这个操作可能是为了确保 CPU 的设置和环境正确初始化。 -
OSStatTaskCPUUsageInit(&err);
:如果 uC/OS-III 的配置中启用了统计任务的 CPU 使用率功能(OS_CFG_STAT_TASK_EN
> 0),则初始化统计任务,用于计算 CPU 使用率。 -
CPU_IntDisMeasMaxCurReset();
:如果 CPU 配置中启用了中断禁用时间测量功能(CPU_CFG_INT_DIS_MEAS_EN
),则重置中断禁用时间的最大值和当前值。 -
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
:如果 uC/OS-III 的配置中启用了轮询调度算法(OS_CFG_SCHED_ROUND_ROBIN_EN
),则配置调度器使用轮询调度,并指定时间片的长度为 1 个系统时钟节拍。
各个任务创建完后,将开始任务挂起,因为开始任务只需要执行一次即可,或者可以将自己删除
3.4 创建各个子任务
void task1(void *p_arg)
{
while(1)
{
OS_ERR err;
p_arg=p_arg;
LED0=0;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
LED0=1;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
}
}
void task2(void *p_arg)
{
while(1)
{
OS_ERR err;
p_arg=p_arg;
LED1=0;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
LED1=1;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
}
}
void task3(void *p_arg)
{
uint8_t key=0;
p_arg=p_arg;
while(1)
{
OS_ERR err;
key=KEY_Scan(0);
if(key==KEY0_PRES)
{
LED0=1;
OSTaskDel(&tcb1, &err);
}
OSTimeDly(500,OS_OPT_TIME_DLY,&err);
}
}
四、总代码
本文将从头文件开始一行行进行代码和相应api函数进行讲解,下面先展示代码源码和实验结果图:
注:本次实验用的开发板为正点原子的探索板
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "includes.h"
#include "os_app_hooks.h"
#include "stm32f4xx_rcc.h"
//start_task
#define START_TASK_PRIO 3
#define star_task_size 256
CPU_STK start_task_stk[star_task_size];
OS_TCB tcb;
void start_task (void *p_arg);
//task1
#define TASK1_PRIO 4
#define task1_size 256
CPU_STK task1_stack[task1_size];
OS_TCB tcb1;
void task1 (void *p_arg);
#define TASK2_PRIO 5
#define task2_size 256
CPU_STK task2_stack[task2_size];
OS_TCB tcb2;
void task2 (void *p_arg);
#define TASK3_PRIO 6
#define task3_size 256
CPU_STK task3_stack[task3_size];
OS_TCB tcb3;
void task3 (void *p_arg);
int main(void)
{
OS_ERR err;
CPU_SR_ALLOC();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);
INTX_DISABLE();
LED_Init();
KEY_Init();
INTX_ENABLE();
OSInit(&err);
OS_CRITICAL_ENTER();
OSTaskCreate( (OS_TCB*) &tcb,
(CPU_CHAR*) "start_task",
(OS_TASK_PTR) start_task,
(void*) 0,
(OS_PRIO) START_TASK_PRIO,
(CPU_STK*) &start_task_stk[0],
(CPU_STK_SIZE) star_task_size/10,
(CPU_STK_SIZE) star_task_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OS_CRITICAL_EXIT();
OSStart(&err);
while(1);
}
void start_task (void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err);
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
#endif
OS_CRITICAL_ENTER();
OSTaskCreate( (OS_TCB*) &tcb1,
(CPU_CHAR*) "task1",
(OS_TASK_PTR) task1,
(void*) 0,
(OS_PRIO) TASK1_PRIO,
(CPU_STK*) &task1_stack[0],
(CPU_STK_SIZE) task1_size/10,
(CPU_STK_SIZE) task1_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OSTaskCreate( (OS_TCB*) &tcb2,
(CPU_CHAR*) "task2",
(OS_TASK_PTR) task2,
(void*) 0,
(OS_PRIO) TASK2_PRIO,
(CPU_STK*) &task2_stack[0],
(CPU_STK_SIZE) task2_size/10,
(CPU_STK_SIZE) task2_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OSTaskCreate( (OS_TCB*) &tcb3,
(CPU_CHAR*) "task3",
(OS_TASK_PTR) task3,
(void*) 0,
(OS_PRIO) TASK3_PRIO,
(CPU_STK*) &task3_stack[0],
(CPU_STK_SIZE) task3_size/10,
(CPU_STK_SIZE) task3_size,
(OS_MSG_QTY) 0,
(OS_TICK) 0,
(void*) 0,
(OS_OPT) OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR,
(OS_ERR*) &err);
OS_TaskSuspend((OS_TCB*) &tcb,&err);
OS_CRITICAL_EXIT();
}
//LED0ÿ500msÉÁ˸һ´Î
void task1(void *p_arg)
{
while(1)
{
OS_ERR err;
p_arg=p_arg;
LED0=0;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
LED0=1;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
}
}
void task2(void *p_arg)
{
while(1)
{
OS_ERR err;
p_arg=p_arg;
LED1=0;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
LED1=1;
OSTimeDlyHMSM(0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT,&err);
}
}
void task3(void *p_arg)
{
uint8_t key=0;
p_arg=p_arg;
while(1)
{
OS_ERR err;
key=KEY_Scan(0);
if(key==KEY0_PRES)
{
LED0=1;
OSTaskDel(&tcb1, &err);
}
OSTimeDly(500,OS_OPT_TIME_DLY,&err);
}
}
五、附加知识
临界区
在并发编程中,临界状态(Critical Section)是指一段代码或操作,其中对共享资源的访问需要进行原子操作,以确保数据的一致性和避免竞态条件的发生。
当多个线程或任务同时访问共享资源时,如果没有适当的同步机制保护,就可能导致竞态条件的发生。竞态条件指的是多个线程或任务之间的执行顺序不确定,导致结果的不确定性或不正确性。
举例来说,假设有两个线程同时访问一个共享的计数变量,并对其进行增加操作。如果这两个线程在不加同步的情况下并发执行,那么可能会出现以下情况:
-
线程 A 读取计数值为 5。
-
线程 B 也读取计数值为 5。
-
线程 A 增加计数值为 6。
-
线程 B 也增加计数值为 6。
在这种情况下,本应该是线程 A 增加后的计数值为 7,但由于并发访问而导致结果不正确。
为了解决这种并发访问共享资源的问题,我们可以使用临界区(Critical Region)或临界段(Critical Section)来保护共享资源的访问。临界区是指一段代码或操作,其中对共享资源的访问需要进行原子操作,即不会被其他线程或任务中断。
在进入临界区之前,通常需要使用同步机制(如互斥锁、信号量等)来保证只有一个线程或任务可以进入临界区执行,其他线程或任务需要等待。一旦一个线程或任务进入临界区,它就可以安全地访问共享资源,执行必要的操作,并在退出临界区时释放同步机制。
通过正确地使用临界区和同步机制,可以确保对共享资源的访问在并发环境下是安全和一致的,避免了竞态条件的发生。这对于并发编程和多任务操作系统的正确性至关重要。
NVIC_PriorityGroupConfig()
优先级分组配置函数。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)
是针对 ARM Cortex-M 处理器的 NVIC(Nested Vectored Interrupt Controller)优先级分组配置函数。
在 ARM Cortex-M 处理器中,NVIC 是用于管理中断的控制器。中断是一种事件,可以中断正在执行的程序并立即处理。NVIC 通过控制中断优先级来确保在同时发生多个中断时的正确处理顺序。
NVIC_PriorityGroupConfig()
函数用于配置 NVIC 的优先级分组方式。优先级分组决定了中断优先级的位数分配,以及子优先级和主优先级之间的优先级级别。根据 ARM Cortex-M 处理器的不同系列,可供选择的优先级分组方式可能有所不同。
在这里,NVIC_PriorityGroup_2
表示选择了 2 位主优先级和 2 位子优先级的优先级分组方式。这意味着在这种配置下,中断优先级可以分为 4 个主组和 4 个子组,共计 16 个不同的优先级级别。
通过设置适当的优先级分组方式,可以更细粒度地控制中断的优先级,并确保系统中断处理的正确性和可靠性。
请注意,具体的优先级分组配置函数和可选的优先级分组方式取决于您所使用的具体 ARM Cortex-M 处理器型号和相应的开发环境。建议查阅相关的文档、参考手册或编程指南,以获取更详细的信息和正确的配置方法。
手动申请任务栈内存
CPU_STK_SIZE stack_size = 1024; // 假设任务栈大小为1024字节
CPU_STK* p_stack = (CPU_STK*)malloc(stack_size * sizeof(CPU_STK)); // 动态分配任务栈内存
if (p_stack != NULL) {
OSTaskCreate(&tcb, "start_task", start_task, NULL, START_TASK_PRIO, p_stack, 0, stack_size, 0, 0, NULL, OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR, &err); // 使用动态分配的任务栈创建任务
} else {
// 内存分配失败的处理
}
在上面的示例中,假设任务栈的大小为1024字节。stack_size
变量用于存储任务栈的大小,单位是字节。
然后,通过调用 malloc()
函数进行内存分配,它用于动态分配内存。malloc()
函数接受一个参数,即要分配的内存大小,以字节为单位。在这里,我们将 stack_size
与 sizeof(CPU_STK)
相乘,以确保分配的内存大小与任务栈的元素数量相匹配。
sizeof(CPU_STK)
是一个 C 语言的运算符,用于计算 CPU_STK
类型的对象所占用的字节数。这是为了确保任务栈的大小适合存储 CPU_STK
类型的元素。
最后,分配的内存将以 void*
类型的指针返回,因为 malloc()
函数返回的是指向分配的内存块的指针。为了与 CPU_STK
类型兼容,我们需要将返回的指针强制转换为 CPU_STK*
类型,以便在后续的任务创建中使用。
总结来说,这段代码的目的是分配一块大小为 stack_size
字节的内存,用于存储任务栈的元素(CPU_STK
类型),并将分配的内存的起始地址存储在 p_stack
变量中,以供后续任务创建使用。