基础结构

本系列属于开发日志,随着开发的深入某些概念/思想可能发生改变。因此,一旦发生前后文冲突,以后文为准;笔者会尽可能标注出准确定义的位置。

HAL 库标准启动流程

  1. 当系统上电时,由 reset 异常触发异常处理。
    1.1. CortexM4 内核自动从 0x00000000 处读取异常向量表首地址(一般为 0x08000000),即 *__initial_sp
    1.2 将 MSP 置位后,继续从向量表中读取并置位 PC 初值。(一般表现为进入 startup.s)

    注意: 笔者在 Ozone 调试时发现复位后有 SP=0x2000 3E30, PC=0x0800 03C4; 同时命令行输出为:

    Target.ReadU32 (0x08000000); // returns 0x4, data is 0x20003E30
    Target.SetReg (“SP”, 0x20003E30);
    Target.ReadU32 (0x08000004); // returns 0x4, data is 0x80003C5
    Target.SetReg (“PC”, 0x80003C5)

    根据 CortexM4 文档易知 C5 表示处理器为 Thumb 状态,而 PC 寄存器却指示为 0x0800 03C4 这里有可能是 Ozone 自动将指示位去掉了。

  2. startup.s
    2.1 使用汇编语言引导进入 SystemInit 函数(在system_stm32f4xx.c实现), 进行系统初始化。该函数需要完成三件事:
    2.1.1 初始化 FPU 。
    2.1.2 初始化额外存储空间 。(外置 RAM)
    2.1.1 初始化 VTOR 。(向量表偏移,见 cortexM4 学习笔记)

    必须定义器件宏,如 STM32F407xx 。该文件需要依赖该宏进行有选择的初始化。

    2.2 使用汇编语言引导进入 main 函数,自此进入 C 的世界。在 main 中需要进行初始化的顺序:
    2.2.1 HAL_Init
    2.2.2 SystemClock_Config
    2.2.3 片上外设初始化(MX_GPIO_Init)
    2.2.4 用户初始化
    2.2.5 while(1)

基础概念

任务与任务切换

任务

  • 重要任务:
    重要任务是开发的核心需求,因为“这个需求”才有了这个开发行为。
    重要任务必须是周期性的,一旦激活则系统会尝试精准调度。

  • 次要任务:
    次要任务是开发核心需求时所产生的一些辅助行为。

  • 内核任务:
    内核任务是操作系统最核心的任务,由该类任务来调度并处理一切其他任务。(注意: 最核心不代表最重要)
    内核任务需要完成的任务:
    + 任务调度
    + 中断处理
    + 维持系统组件的正常运行(如系统时间)

举例:开发某电机控制器。那核心需求就是,这个电机一定是处于被控制状态的,绝对不允许出现跑飞或失控状态。如果产生了一些异常,我希望能将这些异常记录下来,那记录这个行为就是次要任务。

跑飞(定义): 由于未知原因,在没有任何内核异常被触发的情况下,任务无法释放处理器。
对于主要任务,其跑飞表现为不能释放处理器,致使看门狗被触发。
对于次要任务,其跑飞表现为总执行时间超过系统预计时间的 2 倍。
对于内核任务,其跑飞表现为不能释放处理器,致使看门狗被触发。(绝对不允许这种情况发生)

失控(定义):由于程序设计问题,重要/次要任务产生了非预期行为。这不是由内核导致的。

错误(定义):当执行某些操作时,产生了会使预定目标无法实现的行为。如果这个行为能被正常处理,则称其为错误;若不能正常处理(依旧按照未发生错误的情况继续运行),则被归类为失控。
注意: 错误是一种正常运行情况

任务切换

本操作系统不采用时间片轮转式的调度方法。

重要任务
  • 重要任务的切入:
    以任一个重要任务第一次开始执行的时间为基准,所有重要任务都会被尝试周期性进入。

    因为控制算法往往具有积分环节,对时间控制有一定要求,所以重要任务会使用 DWT 为时基。

  • 重要任务的切出:
    重要任务执行时,除看门狗、fault外所有可屏蔽中断都将被屏蔽,并延迟至内核任务时处理。重要任务只有在出现 Fault 时才会被动切出,换言之,任何正常运行的情况下重要任务都不会被动释放处理器。

    • 当出现 fault 时,在调试环境下,内核会执行急停任务并进入锁死状态以保证开发人员解决问题;在应用环境下,则内核将会记录并尝试处理问题,若无法解决则内核会执行急停任务并死锁发出报警。

    重要任务的主动切出存在两种情况:(以传感器多段数据采集,并在采集结束后进行数据处理为例)

    1. 暂时释放:
      裸机情况:触发 DMA 发送信息序列后,异步等待接收中断。并在多段数据均收到后,触发控制算法。
      本内核情况:
      1. 触发 DMA 发送信息序列
      2. 等待中断事件触发(释放,调度进入次要任务)
      3. (触发后)回到重要任务,进行阶段数据解算
      4. 重复上述过程,直至全部接收。
      5. 执行控制算法。

    2. 执行结束:
      本轮算法完成,通知内核将该任务挂起。

    多个重要任务不允许出现交替执行的情况(因为交替执行会出现不稳定的时间顺序)。当一个重要任务被暂时释放时,一定是去执行次要任务的。
    暂时释放时,将存在一个参数,这个参数决定了系统调度的行为(预调度(给定值),预调度(由系统决定),被动调度)。

次要任务
  • 次要任务的切入:
    任何具有足够时间长度的 IDLE 时间段,都将被切换进入次要任务。

  • 次要任务的切出:
    被中断事件打断,或主动释放。

内核任务
  • 内核任务的切入:
    任何重要/次要任务的切出,都将切入内核任务。

  • 内核任务的切出:
    完成系统内定任务后,切换进重要/次要任务。

进程 0

进程 0 是一个特殊的内核任务。系统开始运作后,执行的第一个任务即为进程 0 。
进程 0 负责系统的初始化,且所有空闲时间都将被进程 0 占据。