#前言
1.首先强调一点,“在打比赛中学技术,不是为了打比赛而学技术”,要弱化应试教育的影响,扎扎实实地学技术。
2.希望大家在实践过程中能发现问题,解决问题,理解问题,并及时上传答疑汇总。因为优秀的人会互相学习,之后你也清楚自己需要什么样的队友。
3.调度器思路是借鉴网上资料的,大家可以拿来参考学习,优化自己的程序运行。
#调度器介绍
- 任务调度器是嵌入式系统中的"时间管理大师",它按预定的时间间隔安排不同的任务执行。
- 定时执行任务:按照精确的时间间隔运行特定函数
- 周期性操作:实现循环执行的功能,如LED闪烁、按键扫描
- 非阻塞设计:避免使用延时函数导致的系统阻塞
- 模块化结构:将不同功能分离为独立任务,提高代码可维护性
#代码实现
-
结构体详解
任务调度器的核心是一个精心设计的结构体,它定义了每个任务的关键属性。
/* 调度器任务结构体定义 */
typedef struct
{
void (*task_func)(void); // 任务函数指针
uint32_t rate_ms; // 任务执行周期(毫秒)
uint32_t last_run; // 任务上次执行时间
} scheduler_task_t;
-
task_func
函数指针,指向任务的执行函数。这个函数将在调度时被调用。调用函数必须满足无返回值,无输入值。
-
rate_ms
任务的执行周期,以毫秒为单位。表示该任务应每隔多长时间执行一次。这个参数需要根据实际设置,比如用串口与JY901S陀螺仪通信,由于串口发送速度快,需要实时解算,所以执行周期要快,设置周期为1ms;如果用OLED显示参数,刷新率比人眼快就行,一般设置20ms,或根据显示参数需求设置。
-
last_run
记录任务上次执行的时间戳。用于计算是否到达下次执行时间。
-
任务数组
静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
/* 调度器任务列表 */
static scheduler_task_t scheduler_task[] =
{
/* 实时操作区 */
{Led_Proc, 1, 0}, // LED控制任务:周期1ms
{Key_Proc, 10, 0}, // 按键扫描任务:周期10ms
{Sensor_Proc, 100, 0}, // 传感器读取任务:周期100ms
{Comm_Proc, 50, 0} // 通信处理任务:周期50ms
};
-
初始化函数
计算任务数组的元素个数,并将结果存储在task_num中
uint8_t task_num; // 任务数量
/* 调度器初始化 */
void Scheduler_Init(void)
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t); // 计算任务数量
}
sizeof():获取某个数据类型所占用空间的字节数。
这里通过数组总大小除以单个元素大小,得到数组元素个数。
-
运行函数
遍历任务数组,检查是否有任务需要执行。超过任务的执行周期,则执行该任务并更新上次运行时间
/* 调度器运行 */
void Scheduler_Run(void)
{
uint8_t i;
for (i = 0; i < task_num; i++)
{
uint32_t now_time = uwTick; // 获取当前时间
if (now_time >= (Scheduler_Task[i].last_run + Scheduler_Task[i].rate_ms))
{
Scheduler_Task[i].last_run = now_time; // 更新任务上次运行时间
Scheduler_Task[i].task_func(); // 执行任务
}
}
}
uwTick:定时器时钟计时变量(ms) - 放在定时器里面,每1ms自增
/* 定时器6中断处理函数 */
void TIM6_DAC_IRQHandler(void)
{
if ( TIM_GetITStatus( BASIC_TIM6, TIM_IT_Update) != RESET )
{
uwTick++;
TIM_ClearITPendingBit(BASIC_TIM6 , TIM_IT_Update);
}
}
-
遍历任务数组
循环检查每个任务是否需要执行
-
获取当前时间
调用uwTick获取系统当前时间戳
-
检查执行条件
比较当前时间是否超过了 (上次执行时间 + 执行周期)
-
执行任务
满足条件时,更新上次执行时间并调用任务函数
#主函数参考格式
/* 头文件声明区 */
#include "stm32f4xx.h" //stm32f4x芯片底层驱动头文件
#include "sys.h" //系统初始化底层驱动头文件
#include "delay.h" //延迟底层驱动头文件
#include "led.h" //Led底层驱动头文件
#include "key.h" //按键底层驱动专文件
#include "beep.h" //蜂鸣器底层驱动头文件
#include "oled.h" //OLED7pin底层驱动头文件
#include "BaseTimX.h" //定时器6底层驱动头文件 - 系统定时器专用
#include "time9pwm.h" //定时器9底层驱动头文件 - 舵机专用
#include <Serial.h> //串口1底层驱动头文件 - JY901S陀螺仪专用
#include "usart2.h" //串口2底层驱动头文件 - 四路电机驱动模块专用
#include "openmvuart.h" //串口3底层驱动头文件 - 视觉模块专用
#include <JY901S.h> //JY901S底层驱动头文件
#include "Motor_Ctorl.h" //电机控制底层驱动头文件
#include "Relay.h" //继电器底层驱动头文件 - 驱动电磁铁模块专用
#include "PID.h" //PID底层驱动头文件
#include "Task.h" //主体任务底层调用头文件
/* 测试函数 */
void test(void)
{
}
/* 调度器任务结构体定义 */
typedef struct
{
void (*task_func)(void); // 任务函数
uint32_t rate_ms; // 任务执行周期(毫秒)
uint32_t last_run; // 任务上次运行时间
} task_t;
/* 调度器任务列表 */
static task_t Scheduler_Task[] = {
/* 实时操作区 */
{JY901S_read,1, 0}, // 实时读取JY901S,每1毫秒执行一次
{Encoder_Read,5,0}, // 实时读取编码器值,每5毫秒读一次
{Motor_Ctorl,10,0}, // 实时控制电机任务,每10毫秒执行一次
{Steering_Proc,10,0}, // 实时控制舵机任务,每10毫秒执行一次
{Oled_Proc,20,0}, // 实时更新Oled界面,每20毫秒执行一次
{Key_Proc,20,0} // 实时读取按键变量,每20毫秒执行一次
/* 测试区 */
// {test,5000,0}, // 测试函数,每5000毫秒执行一次
};
uint8_t task_num; // 任务数量
/* 调度器初始化 */
void Scheduler_Init(void)
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t); // 计算任务数量
}
/* 调度器运行 */
void Scheduler_Run(void)
{
uint8_t i;
for (i = 0; i < task_num; i++)
{
uint32_t now_time = uwTick; // 获取当前时间
if (now_time >= (Scheduler_Task[i].last_run + Scheduler_Task[i].rate_ms))
{
Scheduler_Task[i].last_run = now_time; // 更新任务上次运行时间
Scheduler_Task[i].task_func(); // 执行任务
}
}
}
/* 系统初始化函数 */
void System_Init(void)
{
delay_init(168); //delay函数初始化168mhz
/* 设置中断优先级 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);//延时最低优先级
/* 串口初始化 */
Serial_Init(9600);//usart1 - jy901s 波特率9600
Usart2_Init(115200);
Uart3_Init(115200);
/* 配置中断优先级 */
Set_Nvic_Irq(TIM6_DAC_IRQn,0,0);
Set_Nvic_Irq(USART3_IRQn,1,2);
Set_Nvic_Irq(USART2_IRQn,1,1);
Set_Nvic_Irq(USART1_IRQn,1,3);
/* 内设初始化 */
LED_Init();//led初始化
KEY_Init(); //按键初始化
/* 外设初始化 */
OLED_Init();//OLED_Show_InitAll();//oled初始化
OLED_Clear();//OLED_Clear();//清屏
JY901S_Init();//JY901S陀螺仪初始化
Relay_Init();
TIM9_PWM_Init(19999,41);//舵机初始化
PID_jy901s_init();//陀螺仪PID初始化
/* 定时器使能 */
BaseTimx_Init();//使能定时器6
TIM_Cmd(BASIC_TIM6, ENABLE);//使能定时器6
}
int main(void)
{
System_Init();//系统初始化函数
Scheduler_Init();//调度器初始化
Total_Encoder_Read(1);//读取总编码器值使能
while(1)
{
Scheduler_Run();//调度器运行
/* 主体任务 */
if(Task_Flag == 1)//等待初始化完成。。。
{
switch(Task_State)
{
case 1://任务1
Task_1();
break;
case 2://任务2
break;
case 3://任务3
break;
case 4://任务4
break;
}
}
}
}
#实践部分
接下来以老师发的例程“11基本循迹和电机框架+圆环识别”做代码优化实践。
原代码
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "oled.h"
#include "BaseTim6.h"
#include "sr04.h" //
#include "stdbool.h" //bool
#include "xunji.h" //
#include "tb6612.h" //
char lineflag;
char road_flag=0;//=1,正常循迹,=2岔路口
int main(void)
{
u16 i=125;
float y=125.35;
float floatnum1=25.36;
Nvic_Init(NVIC_PriorityGroup_4);//中断分组第四组 高优先打断低优先
Set_Nvic_Irq(USART1_IRQn,0,3);//设置串口1的中断等级;
Set_Nvic_Irq(TIM6_DAC_IRQn,0,3);//设置串口1的中断等级;
Delay_Init();//延时函数默认中断优先级是最低的。
// Sr04_Init();//超声波初始化函数
LED_Init();//LED灯初始化
BEEP_Init();//蜂鸣器初始化
uart1_init(115200);//设置串口1的波特率是115200
Usart_SendByte(USART1,'a');//测试单字节发送函数
Usart_SendByte(USART1,'b');//测试单字节发送函数
Usart_SendString(USART1,"tab\r\n");//测试字符串发送函数
printf("hello world\r\n");//这个也能用,测试printf函数
printf("LED=1,%d\r\n",i);//这个也能用,测试printf函数
Printf(USART1,"LED=0,%d\r\n",i);//这个也能用,测试大写的Printf函数
OLED_Init(); //屏幕初始化
OLED_Clear();
BaseTim6_Init(1);//初始化定时器,同时使能定时器
OLED_Show_InitAll();
OLED_Clear();
xunji_init(); //循迹模块初始化
motor_config_all();//电机初始化
road_flag =1;//正常循迹
while(1)
{
Read_Line_Sensor();
printf("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d...\r\n",s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9]);
if(s[6]==L1)
{
road_flag=2;
}
else if(s[6]==L0)
{
road_flag=1;
}
//***********************8正常循迹
if(road_flag==1)
{
//00100
if(s[1]==L0 && s[2]==L0 && s[3]==L1 && s[4]==L0 && s[5]==L0)
{
lineflag=1;
motor_set_pwm(1,500);
motor_set_pwm(2,500);
}
else if(s[1]==L0 && s[2]==L1 && s[3]==L0 && s[4]==L0 && s[5]==L0)
{
lineflag=2;
motor_set_pwm(1,200);
motor_set_pwm(2,700);
}
else if(s[1]==L1 && s[2]==L0 && s[3]==L0 && s[4]==L0 && s[5]==L0)
{
lineflag=2;
motor_set_pwm(1,0);
motor_set_pwm(2,700);
}
else if(s[1]==L0 && s[2]==L0 && s[3]==L0 && s[4]==L1 && s[5]==L0)
{
lineflag=2;
motor_set_pwm(1,700);
motor_set_pwm(2,200);
}
else if(s[1]==L0 && s[2]==L0 && s[3]==L0 && s[4]==L0 && s[5]==L1)
{
lineflag=2;
motor_set_pwm(1,700);
motor_set_pwm(2,0);
}
}// //***********************8正常循迹
//***********************圆环循迹
//1.停车
//2,转弯
//3、等待转弯结束,条件是什么?
//4、转弯结束之后,又开始正常循迹
else if(road_flag==2)
{
motor_set_pwm(1,0);
motor_set_pwm(2,0);
for(i=0;i<3;i++)
{
BEEP=0;delay_ms(200);
BEEP=1;delay_ms(200);
}
motor_set_pwm(1,400);
motor_set_pwm(2,-300);
delay_ms(200);//根据实际情况调
while(1)
{
Read_Line_Sensor();
motor_set_pwm(1,400);
motor_set_pwm(2,-300);
delay_ms(10);//根据实际情况调
if(s[3]==L1)break;
}
road_flag=1;//正常循迹
}
}
}
结构优化
①添加注释
/* 头文件声明区 */
#include "sys.h" //系统初始化底层驱动头文件
#include "usart.h" //串口底层驱动头文件 - xx专用
#include "delay.h" //延迟底层驱动头文件
#include "led.h" //Led底层驱动头文件
#include "beep.h" //蜂鸣器底层驱动头文件
#include "oled.h" //OLED4pin底层驱动头文件
#include "BaseTim6.h" //定时器6底层驱动头文件 - 系统定时器专用
#include "sr04.h" //超声波底层驱动头文件
#include "stdbool.h" //bool布尔型声明头文件
#include "xunji.h" //循迹模块底层驱动头文件
#include "tb6612.h" //电机模块底层驱动头文件
/* 变量声明区 */
uint8_t lineflag; //巡线标志位 1-正常循迹 2-偏离循迹
uint8_t road_flag=0;//路况标志位 1-正常循迹 2-岔路口
/* 主体函数 */
int main(void)
{
while(1)
{
}
}
补充:关于数据类型的选择,有时候需要选择合适的数据类型,做到高效率的内存管理。比如这次的Line_Flag和Road_Flag,取值范围在0-255,就适合用uint8_t;又比如uwTick定时器计时变量,它取值是成千上万的,所以适合用uint32_t,ms换算成天数,精确到小时,定时器能记满49天17时,跑完整套程序是够用的。
uint8_t / uint16_t / uint32_t /uint64_t 这些数据类型的详解。-CSDN博客
②系统初始化
/* 系统初始化函数 */
void System_Init(void)
{
/* 设置中断优先级 */
Nvic_Init(NVIC_PriorityGroup_4); //中断分组第四组 高优先打断低优先
/* 配置中断优先级 */
Set_Nvic_Irq(USART1_IRQn,0,3); //设置串口1的中断等级;
Set_Nvic_Irq(TIM6_DAC_IRQn,0,3); //设置定时器6的中断等级;
Delay_Init(); //延时函数默认中断优先级是最低的。
/* 外设初始化 */
// Sr04_Init(); //超声波初始化函数
LED_Init(); //LED灯初始化
BEEP_Init(); //蜂鸣器初始化
OLED_Init(); //OLED初始化
OLED_Clear(); //OLED清屏
// OLED_Show_InitAll();
// OLED_Clear();
xunji_init(); //循迹模块初始化
motor_config_all(); //电机初始化
/* 串口初始化 */
uart1_init(115200); //设置串口1的波特率是115200
/* 定时器6初始化 */
BaseTim6_Init(1);
}
/* 串口测试函数 */
void Uart_Test(void)
{
Usart_SendByte(USART1,'a');//测试单字节发送函数
Usart_SendByte(USART1,'b');//测试单字节发送函数
Usart_SendString(USART1,"tab\r\n");//测试字符串发送函数
printf("hello world\r\n");//这个也能用,测试printf函数
printf("LED=1,%d\r\n",125);//这个也能用,测试printf函数
Printf(USART1,"LED=0,%d\r\n",125);//这个也能用,测试大写的Printf函数
}
/* 主体函数 */
int main(void)
{
uint8_t i;//For循环专用变量
float y=125.35;
float floatnum1=25.36;
System_Init();
Uart_Test();
Road_Flag =1;//正常循迹
while(1)
{
}
}
把系统初始化封装到一个System_Init函数中,在main()中while(1)前面执行系统初始化,使代码结构清晰。
③添加uwTick
uint32_t uwTick;//定时器6计时专用变量
在BaseTimX.c文件变量声明区处声明uwTick计时变量(ms)
if(TIM_GetITStatus(BASIC_TIM6, TIM_IT_Update) != RESET )
{
uwTick++;
TIM_ClearITPendingBit(BASIC_TIM6,TIM_IT_Update);
}
在中断处理函数中,实现uwTick++自增
extern uint32_t uwTick;
在BaseTimX.h文件外部变量声明区声明uwTick,便于主函数调用uwTick变量
④插入调度器
/* 调度器任务结构体定义 */
typedef struct
{
void (*task_func)(void); // 任务函数
uint32_t rate_ms; // 任务执行周期(毫秒)
uint32_t last_run; // 任务上次运行时间
} task_t;
/* 调度器任务列表 */
static task_t Scheduler_Task[] = {
/* 实时操作区 */
/* 测试区 */
// {test,5000,0}, // 测试函数,每5000毫秒执行一次
};
uint8_t task_num; // 任务数量
/* 调度器初始化 */
void Scheduler_Init(void)
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t); // 计算任务数量
}
/* 调度器运行 */
void Scheduler_Run(void)
{
uint8_t i;
for (i = 0; i < task_num; i++)
{
uint32_t now_time = uwTick; // 获取当前时间
if (now_time >= (Scheduler_Task[i].last_run + Scheduler_Task[i].rate_ms))
{
Scheduler_Task[i].last_run = now_time; // 更新任务上次运行时间
Scheduler_Task[i].task_func(); // 执行任务
}
}
}
补充:可以调度器可以封装到单独Scheduler.c和Schedule.h文件中,看个人习惯
/* 主体函数 */
int main(void)
{
uint8_t i;//For循环专用变量
float y=125.35;
float floatnum1=25.36;
System_Init(); //系统初始化
Scheduler_Init(); //调度器初始化
Uart_Test(); //串口测试函数
Road_Flag =1; //正常循迹
while(1)
{
Scheduler_Run(); //调度器运行
}
}
配置完调度器底层,就可以用了,不过此时编译会出现以下问题:
这是因为调度器任务列表并未添加任务。接下来我们添加需要执行的任务,这些任务需要定时去读取外部信息,或者定期执行某些功能。比如代码中的读取循迹传感器信息和串口打印数值这两个就符合调度器任务。这里把两个任务整合成一个任务Line_Read()。
/* 循迹读取函数 */
void Line_Read(void)
{
Read_Line_Sensor();
printf("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d...\r\n",s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9]);
}
//...省略
/* 调度器任务列表 */
static task_t Scheduler_Task[] = {
/* 实时操作区 */
{Line_Read,20,0} // 循迹读取函数,每20毫秒读取一次
/* 测试区 */
// {test,5000,0}, // 测试函数,每5000毫秒执行一次
};
补充:函数被调用需要函数在调用处前声明;调度器任务函数必须是无输入,无返回值
{函数名,运行时间间隔,时间初始化(初始化为0)}
如遇到多个任务,花括号{}之间用‘,’,最后一个任务花括号{}后无符号
实现现象
如果代码能正常跑,实验现象与你写的代码目的一致,能够按20ms,5000ms这样的时间间隔去运行,说明你已经基本掌握调度器的用法了,接下来可以大胆使用。
#结尾
1.后面有机会会补充调度器里面的任务优先级
2.接下来准备优化任务管理方面的内容
/* 任务1主体函数 */
void Task_1(void)
{
static uint8_t Task_1_Flag = 0; //任务1执行标志位
static uint8_t Task_1_Step[8] = {0};//任务1步骤执行标志位 0-未执行 1-执行完毕;数组大小由最大步骤数决定
uint8_t Task_1_Step_Temp; //任务1步骤数组或值
/* 读取步骤函数 */
Task_1_Step_Temp = (Task_1_Step[0]<<0) | (Task_1_Step[1]<<1) | (Task_1_Step[2]<<2) | (Task_1_Step[3]<<3) |
(Task_1_Step[4]<<4) | (Task_1_Step[5]<<5) | (Task_1_Step[6]<<6) | (Task_1_Step[7]<<7);
/* 窗口打印函数 */
temp = Task_1_Flag;
/* 任务1主体函数 */
switch(Task_1_Flag)
{
case 0://直走到5号位,拿完,放好
/* 直走一段距离 */
Task_1_Flag = (Run(25,0))?1:0; //Task_1_Flag 未完成-0 任务完成-1
/* 步骤重置函数 */
if(Task_1_Flag == 1)
{
Run(0,1); //直走重置函数
memset(Task_1_Step,0,5); //步骤重置函数
}
break;
case 1://右转90°
switch(Task_1_Step_Temp)
{
case 0x00:
/* 调整90°转向 */
Task_1_Step[0] = (Turn_90(0))?1:0;
break;
case 0x01:
/* 转弯重置函数 */
Turn_90(1);
/* 直走一段距离 */
Task_1_Step[1] = (Run(20,0))?1:0;
break;
case 0x03:
/* 直走重置函数 */
Run(0,1);
Task_1_Flag = 2;
break;
case 0x07:
break;
}
/* 步骤重置函数 */
if(Task_1_Flag == 2)
{
memset(Task_1_Step,0,5); //步骤重置函数
}
break;
}
}
放个代码先,准备先从函数的return(减少函数调用)以及函数重置(提高函数使用率)讲起
准备先从蓝桥杯单片机按键处理函数讲起,再过渡到接下来的任务优化。