目录
一、前后台系统和RTOS系统
1.1 前后台系统
STM32学习的都是裸机形式,通常把程序分为两部分:前台系统和后台系统。前台是中断级,后台是任务级。
- 裸机:没有任何操作系统
- 前台系统:中断
- 后台系统:主程序中的循环
1.2 RTOS系统
RTOS实时操作系统就是类似于linux,windows的一个多任务操作系统。理论上仍然只有一个程序在运行,但它会在各个任务之间进行资源调度,看上去像是在并发执行每一个任务(多线程/多进程)。
- RTOS操作系统:UCOS,FreeRTOS,RTX,RT-Thread,DJYOS等。
- RTOS操作系统的核心内容在于:实时内核。
二、UCOS
UCOS的任务优先级有64个,范围是0~63。都比中断低。
三、UCOS移植
平台上有很多,自己去搜
四、任务创建
任务都是单向不循环的链表
数据域是一个OS_TCB的任务结构体。一般情况下,先独立创建一个起始任务,该起始任务负者初始化中间件,滴答定时器和基本外设。然后再创建其他子任务。要求起始任务的优先级大于其他子任务的优先级,这样起始任务在初始化过程中,不会被其他子任务打断。最后销毁掉起始任务,让其他子任务争夺CPU运行。
4.1 起始任务创建
OSTaskCreate((OS_TCB *)&AppTaskStartTCB, //任务结构体
(CPU_CHAR *)"App Task Start", //任务名称
(OS_TASK_PTR )AppTaskStart, //任务需要执行的函数
(void *)0u, //任务参数
(OS_PRIO )APP_CFG_TASK_START_PRIO,//任务优先级
(CPU_STK *)&AppTaskStartStk[0u], //堆栈空间
(CPU_STK_SIZE )AppTaskStartStk[APP_CFG_TASK_START_STK_SIZE / 10u],//堆栈限深
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,//堆栈空间大小
(OS_MSG_QTY )0u,//消息队列
(OS_TICK )0u,//时间片大小
(void *)0u,//扩展存储器
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务标志位清零
(OS_ERR *)&err);//出错判断结构体
工作任务在AppTaskStart中,只需对它进行改写,在任务函数中,对BSP_Init()进行改写。BSP_Init()函数中存放着模块和片上外设的初始化。
static void AppTaskStart (void *p_arg)
{
OS_ERR err;
(void)p_arg;
BSP_Init(); /* 中间件初始化 */
BSP_Tick_Init(); /* 滴答定时器初始化 */
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err);
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
CPU_IntDisMeasMaxCurReset();
#endif
while (DEF_TRUE)
{
printf("task start\n");
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_DLY,&err);
}
}
在stm32中使用printf时重写puts()函数,需要将stdio.h中的函数全部设置为弱声明。
int fputc(int ch,FILE* f)
{
//printf输出的是串口1
USART_SendData(USART1, ch);
while( USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET );
return ch;
}
4.2 子任务创建
4.2.1 时间片轮转开启
时间片是多个任务/线程/进程在并发执行时,每个任务执行的基本时间单位,它是由滴答定时器来管理的。
例如:设置任务A有10个时间片,任务B有20个时间片。然后1个时间片是1ms。表示任务A运行10ms以后,CPU会切换到任务B运行。任务B运行20ms以后,切换到任务A运行。两者交替。因为时间片一般都很小,所以看上去任务A和任务B是在同时运行。
在os_cfg.h文件中使能sched_round_robin (轮转调度)
#define OS_CFG_SCHED_ROUND_ROBIN_EN 1u
通过修改宏定义为1,开启条件编译,得到函数
#if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u
void OSSchedRoundRobinCfg (CPU_BOOLEAN en, //使能或者失能
OS_TICK dflt_time_quanta,//节拍数的单位
OS_ERR *p_err); //错误管理结构体
注意:一个任务的时间片长度,必须大于它执行一次的时间
当任务调度时间到了后会直接打断当前的运行,导致有的任务没执行完直接被打断,这时候设置临界区。
4.2.2 临界区设置
任务调度是通过时间片累计完成的,当时间片累计到一定程度时切换;而时间片是通过滴答定时器中断累计的,那么反过来,只要关闭中断,时间片就不会再累计,那么任务调度就不会被执行。
临界区的本质: 关闭STM32上的所有中断
CPU_SR_ALLOC(); //临界区初始化
CPU_CRITICAL_ENTER(); //进入临界区
CPU_CRITICAL_EXIT(); //退出临界区
//第一个子任务
static OS_TCB taskA_TCB;
static CPU_STK taskA_Stk[128];
static void taskA(void* p_arg);
//第二个子任务
static OS_TCB taskB_TCB;
static CPU_STK taskB_Stk[128];
static void taskB(void* p_arg);
static void AppTaskStart (void *p_arg)
{
OS_ERR err;
(void)p_arg;
BSP_Init(); /* 中间件初始化 */
BSP_Tick_Init(); /* 滴答定时器初始化 */
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err);
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
CPU_IntDisMeasMaxCurReset();
#endif
//开启时间片调度
OSSchedRoundRobinCfg(ENABLE,0,&err);
//创建子任务A
OSTaskCreate((OS_TCB *)&taskA_TCB, //任务结构体
(CPU_CHAR *)"taskA", //任务名称
(OS_TASK_PTR )taskA, //任务需要执行的函数
(void *)0u, //任务参数
(OS_PRIO )4,//任务优先级
(CPU_STK *)&taskA_Stk[0u], //堆栈空间
(CPU_STK_SIZE )taskA_Stk[APP_CFG_TASK_START_STK_SIZE / 10u],//堆栈限深
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,//堆栈空间大小
(OS_MSG_QTY )0u,//消息队列
(OS_TICK )300u,//时间片大小
(void *)0u,//扩展存储器
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务标志位清零
(OS_ERR *)&err);//出错判断结构体
//创建子任务B
OSTaskCreate((OS_TCB *)&taskB_TCB, //任务结构体
(CPU_CHAR *)"taskB", //任务名称
(OS_TASK_PTR )taskB, //任务需要执行的函数
(void *)0u, //任务参数
(OS_PRIO )4,//任务优先级
(CPU_STK *)&taskB_Stk[0u], //堆栈空间--单位 字
(CPU_STK_SIZE )taskB_Stk[128 / 10u],//堆栈限深
(CPU_STK_SIZE )128,//堆栈空间大小--单位 字
(OS_MSG_QTY )0u, //消息队列
(OS_TICK )600u, //时间片大小
(void *)0u, //扩展存储器
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务标志位清零
(OS_ERR *)&err);//出错判断结构体
//销毁当前任务
OSTaskDel(&AppTaskStartTCB,&err);
}
//子任务A的执行
void taskA(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();//临界区初始化
while(1)
{
CPU_CRITICAL_ENTER(); //开启临界区
printf("taskA running\n");
CPU_CRITICAL_EXIT(); //关闭临界区
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_DLY,&err);
}
}
//子任务B的执行
void taskB(void *p_arg)
{
OS_ERR err;
int ret;
CPU_SR_ALLOC();//临界区初始化
while(1)
{
CPU_CRITICAL_ENTER(); //开启临界区
ret = dht11_begin();
if( ret == 0) //启动成功
{
dht11_get_data();
}
else
{
printf("taskB dht11 failed ret=%d\n",ret);
}
CPU_CRITICAL_EXIT(); //关闭临界区
OSTimeDlyHMSM(0,0,2,0,OS_OPT_TIME_DLY,&err);
}
}
五、时间延时
在裸机系统中,滴答定时器完成毫秒和微秒级别的延时。而在RTOS实时操作系统中,滴答定时器已经被作为时间片的单位使用,所以微秒和毫秒级别的延时,需要使用其他的外设来实现。一共有3种方法。
5.1 使用TIM定时器
初始化某一个定时器为1us触发一次中断并且关闭中断。当需要延时的时候,临时打开该定时器,触发中断则延时结束。
这种方法一般用于高性能的单片机,因为跳转到中断服务函数也是需要时间的,只有主频足够高的情况下,这种延时才有效。
#include "stm32f4xx.h"
volatile uint32_t delay_counter = 0;
//1us触发中断,APB1总线频率为84KHZ
void TIM6_init()
{
TIM_TimeBaseInitTypeDef a;//定时器结构体
NVIC_InitTypeDef b;//中断结构体
//1.使能TIM6的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6 , ENABLE);
//2.时基单元初始化
a.TIM_CounterMode = TIM_CounterMode_Up;//递增模式
a.TIM_Prescaler = 84-1;//预分频数值
a.TIM_Period = 1-1;//重载寄存器数值
a.TIM_ClockDivision = 0;
a.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM6,&a);
//3.配置定时器中断
b.NVIC_IRQChannel = TIM6_DAC_IRQn;//通道54--->TIM6_DAC_IRQHandler
b.NVIC_IRQChannelPreemptionPriority = 0;
b.NVIC_IRQChannelSubPriority = 0;
b.NVIC_IRQChannelCmd =ENABLE;//使能
NVIC_Init(&b);
// 开启定时器中断
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
}
void TIM6_IRQHandler(void)
{
if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
{
// 清除TIM6更新中断标志
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
// 递减延时计数器
if (delay_counter > 0)
{
delay_counter--;
}
}
void delay_us(uint32_t microseconds)
{
// 设置延时结束值
delay_counter = microseconds;
// 启用TIM6中断
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
// 启动TIM6
TIM_Cmd(TIM6, ENABLE);
// 等待延时结束
while (delay_counter > 0);
TIM_Cmd(TIM6, DISABLE);
}
5.2 使用时间戳定时器
时间戳寄存器一般使用来计算代码运行时间的。开始时清空时间戳,运行完毕后获取时间戳数值,该数值就是代码运行的时间
5.3 使用滴答定时器
仍然使用滴答定时器,但是使用时,必须关闭任务调度,然后去获取滴答定时器的计数值来延时。
//滴答定时器延时
//此时滴答定时器已经被开启,不能够去关闭它
//如果关闭了它,那么时间片轮转会出问题,所以这里只能禁止调度,不能关闭所有中断
void delay_ms(int xms)
{
//计算延时时间内需要发生的脉冲次数---滴答定时器中断是自由运行时钟,等于SYSCLK
//定义成int,因为表示负数时要退出循环
int time_cnt = 168000 * xms;
OS_ERR err;
uint32_t now,old;
//关闭任务调度,防止被中途打断
OSSchedLock(&err); //获取当前计数值
old = SysTick->VAL;
while(time_cnt > 0)
{
//获取当前计数值
now = SysTick->VAL;
//计数器没有重载,新数值小于旧数值
if( now < old)
{
time_cnt -= old-now;
}
//发生了重载,新数值大于旧数值
else if( now >= old)
{
time_cnt -= old+(SysTick->LOAD - now);
}
old = now;
}
//解锁任务调度
OSSchedUnlock(&err);
}