FreeRTOS

[English]

FreeRTOS操作系统

简介

  • FreeRTOS是一个迷你的实时操作系统内核。

  • 作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要

  • FreeRTOS能在小RAM单片机上运行。FreeRTOS操作系统是完全免费的开源操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行

FreeRTOS通用架构

FreeRTOS Architecture

FreeRTOS 架构

  • 一个FreeRTOS 系统主要由BSP驱动+内核+组件组成(如上图)。内核包含多任务调度、内存管理、任务间通信的功能,组件包含网络协议、外设支持等。

  • FreeRTOS内核是可剪裁的,组件也是可选的。由于嵌入式应用往往对内存空间的要求十分苛刻,所以一个可剪裁的RTOS对于嵌入式应用非常重要。这使得FreeRTOS的核心代码只有9000行左右。

功能和特点

  • 用户可配置内核功能

  • 多平台的支持

  • 提供一个高层次的信任代码的完整性

  • 目标代码小,简单易用

  • 遵循MISRA-C标准的编程规范

  • 强大的执行跟踪功能

  • 堆栈溢出检测

  • 没有限制的任务数量

  • 没有限制的任务优先级

  • 多个任务可以分配相同的优先权

  • 队列,二进制信号量,计数信号灯和递归通信和同步的任务

  • 优先级继承

  • 免费开源的源代码

FreeRTOS 小结

  • 作为一个轻量级的操作系统,FreeRTOS提供的功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能等,可基本满足较小系统的需要。FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FreeRTOS内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用时间。

  • FreeRTOS的内核可根据用户需要设置为可剥夺型内核或不可剥夺型内核。当FreeRTOS被设置为可剥夺型内核时,处于就绪态的高优先级任务能剥夺低优先级任务的CPU使用权,这样可保证系统满足实时性的要求;当FreeRTOS被设置为不可剥夺型内核时,处于就绪态的高优先级任务只有等当前运行任务主动释放CPU的使用权后才能获得运行,这样可提高CPU的运行效率。

  • 在嵌入式领域,FreeRTOS是不多的同时具有实时性,开源性,可靠性,易用性,多平台支持等特点的嵌入式操作系统。目前,FreeRTOS已经发展到支持包含X86,Xilinx,Altera等多达30种的硬件平台,其广阔的应用前景已经越来越受到业内人士的瞩目。

FreeRTOS SMP架构

SMP简介

  • Armino平台FreeRTOS SMP架构使用的是内核代码路径: components/os_source/freertos_smp_v2p0

  • 当涉及到多处理器系统(Multiprocessor Systems)时,Symmetric Multiprocessing(SMP)是一种常见的架构。

  • BK7258 支持FreeRTOS SMP架构,运行在物理CPU1和CPU2上

  • 以下是对SMP的基本概念、在FreeRTOS中实现SMP的目的和优势的简要介绍:

Symmetric Multiprocessing(SMP)的基本概念

  1. 多处理器系统(Multiprocessor Systems): - 多处理器系统是由多个处理器核心组成的计算机系统。每个处理器核心都能够独立执行任务和运行程序。

  2. 对称性(Symmetric): - SMP系统中,所有处理器核心都具有对称的地位,即它们都能够执行相同的任务和访问相同的系统资源(包括外设及内存)。

  3. 共享内存(Shared Memory): - SMP系统中的处理器核心通过共享相同的内存空间来进行通信。这使得数据共享更加容易,但也需要采取措施来防止竞态条件等问题。

在FreeRTOS中实现SMP的目的和优势

  1. 提高性能: - SMP允许多个处理器核心同时执行任务,从而提高系统整体性能。特别是在处理大量并行任务时,SMP能够有效地分担负载,加速系统的响应速度。

  2. 任务并行执行: - 在SMP系统中,不同的处理器核心可以独立执行不同的任务。这种并行性有助于提高系统的吞吐量和响应能力。

  3. 更好的资源利用: - SMP允许系统在不同的处理器核心上同时执行任务,有效地利用了硬件资源。这对于处理实时任务和对性能要求较高的应用程序非常重要。

  4. 系统灵活性: - SMP架构使系统更具灵活性,可以根据需要扩展处理能力。增加处理器核心的数量可以简化系统的升级和维护过程。

  5. 实时性能: - 对于实时操作系统如FreeRTOS,SMP的实现可以提供更好的实时性能,确保任务在规定的时间内得到执行,从而满足实时系统的要求。

在FreeRTOS中实现SMP需要考虑处理器间同步、共享资源管理和任务调度等方面的挑战,但通过正确的实现,可以显著提高系统的性能和响应能力。

FreeRTOS SMP架构中的调度策略

概述:

SMP模式下的任务调度,仍然遵循基于不同优先级的抢占式和时间片轮询的策略,这和单核架构的任务调度没有任何区别。

不同的是,SMP版本内核采用的是每个core都有一个调度器(即每个core都有自身的滴答中断)。

正是由于这种每个core都有自身调度器的设计,单个core上的调度策略可以认为和单核架构类似,唯一不同的是会考虑core id配置为tskNO_AFFINITY的情形。

总体的调度策略可以归结为:

  • 指定了core id的任务只会在指定的core上运行

  • 当core id配置为tskNO_AFFINITY时,基于内核调度的策略,可能在任意一个core上运行

SMP Schedule

SMP 双核调度示意

任务优先级:

对于FreeRTOS系统,创建任务时设置的优先级数字越大,则该任务的优先级越高。每次执行任务切换时,系统总是从就绪列表中选择当前优先级最高的任务执行。

在SMP模式下,每个核独立地调度要运行的任务。当某个核选择一个任务时,该核会选择优先级最高且可以在该核上运行的就绪状态任务。

满足以下条件时,代表任务可以在核上运行:

  • 任务亲和性兼容,任务已分配给当前核或core id配置为tskNO_AFFINITY;

  • 该任务当前没有在其他核上运行。

需要注意的是,两个具有最高优先级的任务不一定始终由调度器运行,因为还需要考虑任务配置的核亲和性。

如以下示例(注意此处的优先级是从内核视角看到的,所以数字越大,优先级越高):

  • 任务A的优先级为8,分配给核0

  • 任务B的优先级为7,分配给核0

  • 任务C的优先级为6,分配给核1

经过调度后,任务A将在核0上运行,任务C将在核1上运行。即使任务B是第二优先级的任务也不被执行(因为其只分配给了核0)。

任务抢占:

在单核的FreeRTOS系统中,如果优先级更高的任务已准备好执行,调度器可以抢占当前正在运行的任务。在SMP系统中,如果调度器确定一个优先级更高的任务可以在某个核上运行,那么调度器可以单独抢占各个核。

但在某些情况下,一个优先级更高的就绪任务可以在多个核上运行。此时调度器只会抢占一个核。即使当前有多个核可以抢占,调度器总是优先选择当前核。

比如给定以下任务:

  • 任务A的优先级为6,当前在核0上运行

  • 任务B的优先级为7,当前在核1上运行

  • 任务C的优先级为8,没有分配给固定的核,但是由任务B解除了阻塞

那么经过调度后,任何A继续在核0上运行,任务C将抢占任务B。即优先级为6的任务在核0上运行,优先级为8的任务在核1上运行,而优先级为7的任务没有运行。

任务时间片:

对于单核模式下的FreeRTOS,如果当前最高的优先级包含多个就绪任务,调度器会在这些任务间轮询定期切换。

但是对于SMP内核,特定任务可能无法在特定核上运行,因此时间片轮询策略和单核的有差异。造成这种现象的原因可能是:

  • 任务分配给了另一个核

  • 任务未分配,但已经由其他核运行

基于此,当核在所有就绪状态任务中搜索寻找要运行的任务时,可能需要跳过同一优先级列表中的一些任务或者降低优先级,以找到可以运行的就绪状态任务。SMP内核调度器会确保已选择运行的任务至于列表末尾,为同一优先级的就绪状态任务时间最佳轮询时间分片。这样在下一次调度中,未经选择的任务优先级会更高。

备注

为了实现理想的时间片轮询,建议将特定(上层需要完美时间分配)优先级的所有任务都分配给同一个核。

滴答时钟:

在SMP内核中,每个核都有自己的滴答中断,都会定期接收到滴答中断并独立运行。每个核上的滴答中断周期相同,但可能不同步。

需要注意的是核0以及核1上对滴答中断的处理有区别。

核0上的内容与单核系统中的一致,主要负责:

  • 增加内核的的滴答计数

  • 检查是否有任务超时到期,到期则解除其阻塞状态,加入就绪列表

  • 检查是否需要进行时间片轮询

  • 执行应用层回调函数

核1仅检查是否需要任务切换,以及执行应用层提供的滴答钩子函数(如有)。

任务恢复与挂起:

在SMP内核中,无法在多个核上同时挂起调度器。只能在特定的核上调用vTaskSuspendAll()和xTaskResumeAll()。

当在某个核上调用vTaskSuspendAll()时:

  • 只在该核上禁用任务切换,但该核上的中断仍然可以响应;

  • 该核上不再响应任何阻塞或主动引发调度的行为,也不再进行时间分片

  • 该核上的中断解除任务阻塞时,如果亲和性配置为该核的任务会进入该核的待执行任务列表。未分配的任何或亲和性配置为其他核的任何可以在运行调度器上的核上进行调度

  • 所有核上的调度器均挂起时,由中断解除阻塞的任何将进入他们分配的核上的待执行任务列表。如果任务未分配,则进入调用中断的核上的待执行任务列表。

  • 如果该核为core 0,则计数器将被暂停,挂起的时钟计数将递增,但仍会发生时钟中断以执行应用程序时钟。

在某个核上调用xTaskResumeAll()时:

  • 任何添加到该核的待执行任务列表中的任务将恢复运行;

  • 如果该核为core 0,则挂起的时钟计数将被补偿。

空闲任务:

SMP系统为每个核单独创建一个固定的空闲任务,每个核上的空闲任务与单核模式下的相同。

软件定时器:

SMP的软件定时器默认创建在Core0上,并且被设置为最高优先级。

备注

如果同为core0上的任务,使用软件定时器可以看成同步的,因为此时软件定时器的优先级最高。如果任务可能运行在其他core上, 那使用软件定时器可能为同步也可能为异步,需要上层应用根据业务场景自行处理。

在FreeRTOS SMP架构中资源同步

SMP Architecture Resource
SMP Architecture Resource Synchronization

SMP 架构共享资源同步

  1. Task间共享资源同步:

    • 操作系统的信号量(Semaphore)

    • 操作系统的同步锁(Mutex)

    • 关调度

  2. ISR间共享资源同步:

    • 自旋锁(SpinLock)

  3. Task/ISR间共享资源同步:

    • 自旋锁(SpinLock)

FreeRTOS SMP架构中自旋锁与临界区

自旋锁:

对于SMP系统,共享资源的访问依赖于自旋锁。

实现原理

自旋锁是一种用于保护多线程共享资源的锁,当一个线程尝试去获取某一把锁时,如果这个锁此时已经被别人获取,那么此线程就无法获取到这个锁,那么会一直等待,直到拿到这个锁。

一般只有在多核系统中才会使用到自旋锁。它实现的最关键一点是保证原子性操作。对于不同的硬件架构,通常借用架构提供的原子指令来实现。

动态自旋锁和静态自旋锁

为了在保证对共享资源保护的基础上提高系统的性能,自旋锁分别静态自旋锁与动态自旋锁。所有自旋锁资源在内存中位于固定的内存段sram_spinlock_section,由宏CONFIG_SPINLOCK_SECTION控制。

其中静态自旋锁供应用层使用,具体的用法可以参考下面的章节;而动态自旋锁供内核使用,每申请一个内核对象,就会为该内核对象申请一个动态自旋锁。

备注

需要注意的是,动态自旋锁的资源与申请的对象绑定,只有在该内核对象被释放后,动态自旋锁的资源才会回收。比如申请了一个队列资源,内核会为这个队列申请一个动态自旋锁,只有在该队列被销毁时,动态自旋锁的资源才会被回收。

自旋锁API

自旋锁相关的代码实现在middleware/driver/spinlock路径下,当前的自旋锁有如下的API:

  • 获取一个自旋锁,未获取到的core会自旋:

    void spin_lock(volatile spinlock_t *lock)
    
  • 检查当前的自旋锁是否已经被占有,如果锁空间则获取锁:

    int spin_trylock(volatile spinlock_t *lock)
    
    返回值:1:自旋锁已被占用 0:自旋锁未被占用
    
  • 释放一个自旋锁:

    void spin_unlock(volatile spinlock_t *lock)
    
  • 屏蔽中断并获取自旋锁:

    uint32_t _spin_lock_irqsave(spinlock_t *lock)
    
  • 释放自旋锁并恢复中断:

    void _spin_unlock_irqrestore(spinlock_t *lock, uint32_t flags)
    

备注

当前的自旋锁支持嵌套机制,最大支持256级的嵌套

自旋锁的使用

应用层使用自旋锁可以参考如下的步骤:

  • 初始化一个自旋锁:

    #ifdef CONFIG_SOC_SMP
    #include "spinlock.h"
    static SPINLOCK_SECTION volatile spinlock_t test_spin_lock = SPIN_LOCK_INIT;
    #endif // CONFIG_SOC_SMP
    
  • 获取自旋锁:

    spin_lock(&test_spin_lock);
    
  • 释放自旋锁:

    spin_unlock(&test_spin_lock);
    

备注

spinlock必须放在SPINLOCK_SECTION段中

临界区:

在AMP架构下,FreeRTOS进行临界区的操作为屏蔽中断。以防止在临界区内发生抢占式上下文切换和中断服务,确保进入临界区的任务或ISR是访问共享资源的唯一实体。 对于SMP架构,临界区的操作变得更加复杂,仅仅是关闭中断并不能保护一些共享资源,如内存、外设等,因为其他核此时仍能访问这些共享资源。 这时候就需要借助上面介绍的自旋锁功能,所以对于SMP的临界区,实际上是中断屏蔽和自旋锁的组合。

临界区的使用:

临界区的使用有两种方法:

1.应用层直接调用由OS适配层提供的API,,这种情况下所有的调用者共用一把锁:

  • 进入临界区:

    uint32_t rtos_enter_critical( void )
    {
            uint32_t flags = rtos_disable_int();
            spin_lock(&rtos_spin_lock);
            return flags;
    }
    
  • 退出临界区:

    void rtos_exit_critical( uint32_t state )
    {
            spin_unlock(&rtos_spin_lock);
            rtos_enable_int(state);
    }
    

2.模块自己定义一把锁,按下面的方式封装,这种方式模块使用自己的锁:

#include "spinlock.h"

static SPINLOCK_SECTION volatile spinlock_t xx_spin_lock = SPIN_LOCK_INIT;

static inline uint32_t xx_enter_critical()
{
    uint32_t flags = rtos_disable_int();

#ifdef CONFIG_FREERTOS_SMP
    spin_lock(&xx_spin_lock);
#endif // CONFIG_FREERTOS_SMP

    return flags;
}

static inline void xx_exit_critical(uint32_t flags)
{
#ifdef CONFIG_FREERTOS_SMP
    spin_unlock(&xx_spin_lock);
#endif // CONFIG_FREERTOS_SMP

    rtos_enable_int(flags);
}

备注

建议:如果受保护的资源访问频次有限,可以采用方法一,如果是上层需要频繁访问的资源,可以采用方法二。 另外,除非是确定必须使用临界保护的资源才建议使用临界区,其他情况推荐使用mutex。

注意事项

由于临界区禁用了中断以及需要获取自旋锁,在使用临界区时,应注意以下的限制:

  • 临界区应尽可能短,如果在临界区中耗时过长(如超过一个tick的时间),则会影响系统的调度性能

  • 不应在临界区内调用FreeRTOS API

  • 不应在临界区内调用任何阻塞函数

  • 如有可能,将尽可能多的执行操作或事件处理推到临界区之外

  • 典型的临界区应该只访问少数数据结构或硬件寄存器

在FreeRTOS SMP架构中API介绍

通常情况下,应用层不应该直接调用内核提供的API,而应该调用SDK中OS 适配层提供的API。适配层的源码位于:components/bk_rtos/freertos/v10/rtos_pub_smp.c中,对应的头文件位于:os/os.h中。

具体可参考:

FreeRTOS SMP架构工程配置

BK7258 ap目录默认app工程是SMP架构,工程配置如下:

CONFIG_CPU_CNT=2
CONFIG_SOC_SMP=y
CONFIG_FREERTOS_SMP=y
CONFIG_SPINLOCK_SECTION=y