0,序章

        RTOS 的核心:交替执行

        比如一个职场妈妈需要同时给小孩喂饭和回复同事信息,于是她左手拿勺子右手敲键盘。可是脑子只有一个,无法真正的“一心多用”,于是她加快反应速度,上一秒考虑给小孩夹哪个菜,下一秒考虑给同事回复什么信息,本质上是“交叉执行”。

        课程风格:先设计(要解决什么问题),然后讲解 FreeRTOS 提供的解决方法,讲解 FreeRTOS 的API 及内部原理(只是原理性介绍,内部源码的深入讲解建内部机制的课程)。

        程序效果:实时运行三个项目(音乐播放、打砖块游戏、汽车游戏),框架如下:

        

核心是打砖块游戏:通过红外遥控器、旋转编码器、MPU6050三种方式操控挡球板移动。

1,单片机程序设计模式

        前三种模式无法解决一个问题:假设 A、B 两个都是很耗时的函数,无法降低它们之间的影响。第四重方法可以解决此问题,但实践起来很麻烦。

1)轮询模式

        一个 while 循环里依次调用2个函数,如果一个程序耗时长,那么另一个程序迟迟无法调用,只适合简单的 耗时短的场景。

2)前后台

        一个函数在while中持续调用,另一个通过中断调用。通过中断调用的函数非常及时,如果中断触发的函数太花时间,则导致后台程序(while循环中的函数)迟迟无法执行。

        继续改进:不设置后台程序,两个函数都用前台程序的方式(中断驱使),此时如果两个任务同时发起,就会耽误其中一个(不同时、相近发出则无影响)。

3)定时器驱动

        前后台模式的一种,可以按照不同评率触发定时器产生中断,让中断函数在合适的时间调用对应的函数。(适合调用周期性函数,并且每一个函数执行的时间不能超过一个定时器周期。无法避免轮询模式的缺点:函数相互之间有影响。)

4)基于状态机

        可以避免函数相互之间的影响。还是在 main 函数里用轮询模式依次调用 2 个函数,关键在于2个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间。(以“喂饭”为例,函数可拆分为:舀饭、喂饭、舀菜、喂菜;若拆分后每一步只需 1 秒,就降低了对后面任务的影响。)

        问题是很多场景里函数不易拆分为多个状态,并且这些状态执行的时间并不好控制。

        所以这并不是最优的解决方法,需要用到多任务系统。

2,多任务系统

1)多任务模式

        "职场妈妈的困境" :假设职场妈妈要调试负责给小孩喂饭(需要 t1~t5 五段时间)和给同事回信息(需要 ta~te 五段时间 ),在裸机程序上无论用什么模式进行精心的设计都无法避免轮流执行的问题。

        就如同序章里所说的心灵手巧的职场妈妈,RTOS 程序解决此问题的方式是:交叉执行, t1~t5 和 ta~te 交叉执行的示例,如下图所示。

// RTOS 程序示例代码
喂饭任务()
{
    while (1)
    {
        喂一口饭();
    }
}

回信息任务()
{
    while (1)
    {
        回一个信息();
    }
}


void main()
{
    // 创建2个任务
    create_task(喂饭任务);
    create_task(回信息任务);

    // 启动调度器
    start_scheduler();
}

        启用调度器后,两个任务就会交叉执行了。

        多任务系统通过依次给任务分配时间,只有切换的间隔足够短,用户就会“感觉这些任务在同时运行”。如下图所示:

2)互斥操作

        多个任务可能会“同时”访问某些资源,需要增加保护措施以防止混乱。

        比如两个任务都要用串口,可用全局变量让它们独占地、互斥地使用串口(此操作不保险,后续会具体讲解解决方案)。

int g_canuse = 1;

void uart_print(char *str)
{
    if(g_canuse)
    {
        g_canuse = 0;
        printf(str);
        g_canuse = 1;
    }
}

// 后续的串口调用都使用 uart_print() 接口
task_A()
{
    while(1)
    { uart_print("0123456789\n"); }

}

task_B()
{
    while(1)
    { uart_print("abcdefghij\n"); }

}

void main()
{
    // 创建2个任务
    create_task(task_A);
    create_tasl(task_B);

    // 启动调度器
    start_scheduler();
}

        只要运行的时间足够长,还是会出现数字、字母混杂的情况。需要从原理上理解,把 uart_print 函数标记为4个步骤:

void uart_print(char *str)
{
    if( g_canuse )    ①
    {
        g_canuse = 0; ②
        printf(str);  ③
        g_canuse = 1; ④
    }
}

        如果task_A 执行完①,进入if语句里面执行②之前被切换为task_B:在这一瞬间, g_canuse 还是 1。task_B 执行①时也会成功进入if语句,假设它执行到③,在printf打印完部分字符 比如“abc”后又再次被切换为task_A。 task_A 继续从上次被暂停的地方继续执行,即从②那里继续执行,成功打印出 “0123456789”。这时在串口上可以看到打印的结果为:“abc0123456789”。

        换一种思路再尝试:

void uart_print(char *str)
{ 
    g_canuse--;         ① 减一
 
    if( g_canuse == 0 ) ② 判断
    {
        printf(str);    ③ 打印
    }

    g_canuse++;         ④ 加一
}

        仍然可能产生两个任务同时使用串口的情况。因为“①减一”这 个操作会分为3个步骤:a.从内存读取变量的值放入寄存器里,b.修改寄存器的值让它减一, c.把寄存器的值写到内存上的变量上去。如果task_A执行完步骤a、b,还没来得及把新值写到内存的变量里,就被切换为task_B: 在这一瞬间,g_canuse还是1。task_B 执行①②时也会成功进入if语句。

        综上,“互斥操作”在多任务系统编写程序时非常重要,任何一种多任务系统都会提供相应的函数。

        

3)同步操作

        如果任务间有依赖关系,比如任务A 执行某操作后,需要任务B 进行后续的处理。

        错误方案(任务B 大部分时间做的都是无用功)

int flag = 0;


void task_A(void)
{
    while (1)
    {
        // 执行复杂的业务逻辑运算
        // ...

        // 任务完成,置位标志位通知任务B
        flag = 1;
    }
}


void task_B(void)
{
    while (1)
    {
        // 持续轮询,flag未置位时空循环,浪费CPU资源
        if (flag)
        {
            // 执行后续的处理操作
            // ...
        }
    }
}


int main(void)
{
    // 创建任务A和任务B
    create_task(task_A);
    create_task(task_B);

    // 启动RTOS调度器
    start_scheduler();
}

        如果可以让任务B 阻塞(即不参与调度),那么任务A 处理完后再唤醒任务B ,此方法可有效利用 CPU 资源。

void task_A(void)
{
    while (1)
    {
        // 执行复杂的业务逻辑运算
        // ...

        // 任务完成,释放信号量,唤醒阻塞中的任务B
        semaphore_give(sync_sem);
    }
}

void task_B(void)
{
    while (1)
    {
        // 等待信号量,未获取时进入阻塞态,释放CPU
        semaphore_take(sync_sem);

        // 获取到信号量后,执行后续的处理操作
        // ...
    }
}

int main(void)
{
    // 初始化信号量(确保任务B先阻塞)
    semaphore_init(sync_sem, 0);

    create_task(task_A);
    create_task(task_B);

    // 启动RTOS调度器
    start_scheduler();
}

        此过程中,任务A 处理复杂事情的时候可以独占 CPU 资源,加快处理速度。

3,创建 FreeRTOS 工程

        准备操作参照文档第三、四章(搭建开发环境、开发板硬件连接、调试编译下载),第五章有不同模块的配置与使用说明。

        启动 STM32CubeMX 软件,参照文档第六章内容创建新工程(选择芯片、配置时钟、配置GPIO、FreeRTOS(含时基频率、任务堆栈大小、互斥锁等等)),生成Keil MDK 工程,如下:

        用 Keil 打开工程,是在工程的“MDK-ARM”目录下,双击如下文件:

        之后可按照开发者需求修改代码。快速入门的编写方式:打开freertos.c文件,找到StartDefaultTask函数里的循环。编写的代码, 需要位于“USER CODE BEGIN xxx”和“USER CODE END xxx”之间,否则以后再次使用 STM32CubeMX 配置工程时,不在这些位置的用户代码会被删除

创建简单的多任务程序

        目标:创建两个任务,一个运行Led_Test,一个运行LCD_Test

        创建新任务不同操作系统的方法不一样,cmsis_os2提供了统一的接口层【如FreeRTOS、RT-Thread等不同操作系统都要与接口层相对应】,练习时用 freertos 的原生代码创建任务即可。

        1)默认任务处理LCD

void StartDefaultTask(void *argument)
{
    /* USER CODE BEGIN StartDefaultTask */
    /* Infinite loop */
    LCD_Init();
    LCD_Clear();

    for(;;)
    {
        //Led_Test();
        LCD_Test();
    }
}

        2)创建另外的任务

        参考:

/* USER CODE BEGIN FunctionPrototypes */
void MyTask(void *argument)
{
    while(1)
    {
        LED_Test();
    }
}

// 默认任务和新创建的任务,优先级都为 Normal
defaultTaskHandle = osThreadNew(StartDefaultTask, Null, &defaultTask_attributes);

// USER CODE BEGIN RTOS_THREADS
xTaskCreate(MyTask, "myfirsttask", 128, NULL, osPriorityNormal, NULL);

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐