嵌入式系统

1.嵌入式系统的基本运行模式

嵌入式系统的功能就是通过传感器周期性地获得外部数据,进行分析和计算后输出控制指令,运行期间接受用户输入信息、显示系统信息1

2.嵌入式软件系统结构

嵌入式软件结构可以分为轮询系统前后台系统多任务系统2

2.1 轮询系统

轮询系统主程序是一段无限循环的代码,在循环中顺序查询各个条件,如果满足就执行相应的操作。这种方案的好处是实现简单,逻辑清晰,便于开发人员掌握。但是每个事件的查询和处理时间是不能确定的。假如前面的操作时间较长,那么后面的操作必然会被延迟,系统的性能和响应能力差。

//基于周期调用运行模式伪代码
while(1)
{
    传感器采样数据读取();
    用户输入命令检查();
    传感器采样数据读取();
    数据计算及其控制输出();
    传感器采样数据读取();
    状态显示更新();
    传感器采样数据读取();
    数据计算及其控制输出();
}

优化方法

  • 运行时间长的任务进行拆分

假设状态显示更新任务由于LCD特性,运行慢。

void 状态显示更新()     
{
    更新测量数据曲线图();
    更新数据计算结果();
    更新用户输入内容();
}

进行拆分,分3个阶段执行显示更新。

void 状态显示更新()   
{ 
    static int state = STAGE0;
    switch(state):
    {
        case STAGE0:
            更新测量数据曲线图();
            state = STAGE1;
            break;

        case STAGE1:
            更新数据计算结果();
            state = STAGE2;
            break;

        case STAGE2:
            更新用户输入内容();
            state = STAGE3;
            break;
    }
}
  • 时间格点采样

如果相邻两次调用读取数据函数间隔时间小于传感器两次采样间隔,可通过在固定特定时间格点读取数据解决。

void 传感器采样数据读取()
{
    while((读入当前时间() % 采样时间间隔) > 0)
        延迟等待固定时间();
    读取并保存数据();
}

很多情况,采样时间间隔可通过外部硬件确定,用户程序通过读取FIFO,当数据尚未到达时,FIFO为空。因此,可基于FIFO是否为空来判断时间格点上的数据采集是否完成。

void 传感器采样数据读取()
{
    while(外部FIFO空())
        延迟等待固定时间();
    读取并保存数据();
}

如果读取传感器数据间隔大于采样间隔,将导致采样点遗漏。

2.2 前后台系统

相对轮询系统,前后台系统对外部事件的处理做了优化。前后台系统是由中断驱动的。主程序依然是一段无限循环的代码,称为后台程序,而事件的响应则由中断来完成,称为前台程序。

在后台程序执行的时候,如果有外部事件发生,则前台的中断程序会打断后台程序。在完成必要的事件响应之后,前台中断程序退出并通知后台程序来继续操作。由后台程序完成事件的后继处理,比如数据的分析等操作。从代码功能上讲,事件的响应和处理分为了两个部分。因为中断自身有优先级和嵌套的功能,所以优先级高的事件能够得到及时响应。但后台程序仍然需要按顺序的处理各个事件的后继事务。

使用中断来代替轮询方案中事件的查询操作,所以相对轮询方案,前后台系统对事件的响应能力有较大改善。

使用前后台模式时,后台中断服务程序尽量放较少的代码,把对实时性要求不高的操作放到前台程序中。一个极端示例如下,所有操作都放到中断服务程序,主程序只有一个无限循环。

void main()
{
    while(1)
    {

    }
}

void 时钟中断服务1() //硬件定时器1,8个时间片
{
    状态显示更新();
}

void 时钟中断服务2()    //硬件定时器2,8个时间片
{
    用户输入命令检查();
}

void 时钟中断服务3()    //硬件定时器3,4个时间片
{
    数据计算及控制输出();
}

void 时钟中断服务4()    //硬件定时器4,2个时间片
{
    传感器采样数据读取();
}

注:时间片指的是任务一次投入运行,在不被抢占或者中断的情况下,能够连续执行的最长时间(以时钟节拍计数)。

基于中断服务程序实现所有操作需要处理数据访问冲突问题!需要可靠的数据交换机制,确保不同代码能够安全访问共享数据交换空间。一种常用方法是使用环形缓冲器。

环形缓冲区是嵌入式系统中十分重要的一种数据结构,比如在串口处理中,串口中断接收数据直接往环形缓冲区丢数据,而应用可以从环形缓冲区取数据进行处理,这样数据在读取和写入的时候都可以在这个缓冲区里循环进行,程序员可以根据自己需要的数据大小来决定自己使用的缓冲区大小3

环形缓冲区是队列的一个应用,环形缓冲区(环形队列)的实现:简单来说,这其实就是一个数组,只不过有两个指针,一个指向列队头,一个指向列队尾。指向列队头的指针(Head)是缓冲区可读的数据,指向列队尾的指针(Tail)是缓冲区可写的数据,通过移动这两个指针(Head) &(Tail)即可对缓冲区的数据进行读写操作了,直到缓冲区已满(头尾相接),将数据处理完,可以释放掉数据,又可以进行存储新的数据了。

使用环形缓冲区可以减小数据丢失的风险,更加保证了数据的安全性和有效性4

/*BUF_SIZE就是一个宏大小一般定义为128*/
static int Buf[BUF_SIZE]={0}; //缓冲区
static int W=0; //写指针
static int R=0; //读指针

/*环形缓冲区初始化*/
void Buff_Init(void)
{
    Buff_Clear(); 
}

/*写数据*/
void Buff_Write(int data)
{
    if((W+1)%BUF_SIZE!=R) //队列未满
    {
        Buf[W]=data;
        W=(W+1)%BUF_SIZE;
    }
}

/*读数据*/
int Buff_Read(void)
{
    int data=0;
 
    if(R!=W) //队列非空
    {
        data=Buf[R];
        R=(R+1)%BUF_SIZE;
    }
    return data;
}

/*环形缓冲区清0*/
void Buff_Clear(void)
{
    W=0;
    R=0;
}

  • 基于事件队列的运行模式
  • 带时间信息的事件队列运行模式
  • 计算图运行模式

2.3 多任务系统

和前后台系统相比,多任务系统在响应事件的时候,同样是由多个中断处理程序完成的。但是对于事件的后继操作则是由多个任务来处理的。也就是说每个任务处理它所负责的事件。在基于优先级的多任务系统中,因为任务间优先级的关系,那么优先级高的任务可得到优先处理。这样优先级高的事件就能及时得到处理;在基于分时机制的多任务系统中,则任务间按比例轮流占用处理器。

从事件和数据处理的角度考虑,可以把整个应用流程简化为事件响应和事件处理两个阶段。正是对着两个阶段的采用的不同技术手段,才逐步发展出上面介绍的这三种软件结构方案。从这个发展路线可以明显感觉到的就是解决问题的思路越来越清晰,结构和层次越来越合理。以下是对三种软件结构的比较。

多任务系统,指的就是基于多任务操作系统的应用开发模型。核心部件是嵌入式实时操作系统内核的设计和实现,主要功能包括:任务管理、任务调度、任务同步、互斥和通讯、设备管理、中断管理、时间管理等。而像图形用户接口、文件系统、TCP/IP 协议、嵌入式数据库引擎等,则可以归为内核层之上的嵌入式操作系统的功能模块。

时钟概念5
  • 晶振频率
    • 晶振,全称为晶体振荡器,能够给单片机(MCU)提供一个工作的信号,也就是所谓的时钟信号,这个信号能够促使单片机(单片机)有条不紊地运行下去。
    • 频率是单位时间(1s)内某件事周期性变化的次数。晶振频率是晶体振荡器的固有频率, 不能改变。若晶振频率为12MHz,则在1s的时间内,0 、1电压周期性变化了12_000_000次。
  • 时钟周期
    • 时钟周期,一般也称振荡周期。如果晶振的输出没有经过分频或倍频就直接作为cpu的工作时钟,则时钟周期就等于晶振的振荡周期;如果晶振的输出经过分频或倍频后作为cpu的工作时钟,则时钟周期就就是分频或倍频后的。即,时钟周期是CPU的实际工作频率的倒数。在一个时钟周期内,CPU仅完成一个最基本的动作。
  • 时钟频率
    • 时钟协调整个电路中所有元件协同工作。时钟频率=1秒/时钟周期。
  • 时钟节拍
    • 时钟节拍是特定的周期性的中断。为了生成时钟节拍,往往需要使用一个硬件定时器,配置硬件定时器产生一个频率为10~1000Hz之间的中断,当时钟中断发生时,中断服务程序调用操作系统内核中一个特殊程序:系统时钟节拍服务。
  • 机器周期
    • 一条指令的执行划分为若干个阶段, 每一个阶段完成一项基本任务,如: 取指令、存储器读、存储器写等, 这每一项工作称为一个基本操作,完成一个基本操作所需要的时间为机器周期。一般情况下,一个机器周期由若干个状态周期(时钟周期)组成。
  • 指令周期
    • 指令周期是执行一条指令所需要的时间,即CPU从内存取出一条指令并执行这条指令的时间总和。一般由若干个机器周期组成,从取指令、分析指令到执行完所需的全部时间。指令不同,所需的机器周期数也不同。
  • 总线周期
    • 访问1次存储器和I/O端口操作所需要的时间。
多任务机制

在多任务操作系统内核中,必须提供解决并发任务的机制。通用内核一般以“进程”、“线程”等为单位来管理用户任务。但在很多嵌入式内核中,并没有区分进程和线程,只是把整个内核当作一个大的运行实体,其中运行着很多任务。任务通常作为调度的基本单位。

进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

  • 进程
    • 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
  • 线程
    • 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。 与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程6
任务上下文

任务可以看作是用户程序在处理器等硬件上的运行,是一个动态的概念。

任务在处理器上运行的某一时刻,有它自己的状态,即处理器所有的寄存器的数据,这个叫做任务的上下文,可以理解为是处理器的“寄存器数据快照”。通过这些数据,内核可以随时打断任务的运行或者加载新的任务,从而实现不同任务的切换运行。

任务切换

任务切换又叫做任务上下文切换。当内核需要运行其它的任务时,内核首先会保存和当前任务相关的寄存器的内容到当前任务的栈中;然后从将要被加载的任务的栈中取出之前保存的全部寄存器的内容并加载到相关的寄存器中,从而继续被加载任务的运行。这个过程叫做任务切换。

任务的时间片和优先级

时间片指的是任务一次投入运行,在不被抢占或者中断的情况下,能够连续执行的最长时间(以时钟节拍计数)。时间片的长度由具体内核规定,有些内核中不同任务可以有不同的时间片长度,或者是在运行过程中可以动态改变时间片长度。

时间片长度是时钟节拍的整数倍,一个时钟节拍被定时器 ISR 和任务共享。。

时间片和时钟节拍关系

时间片和优先级是任务的两个重要参数,分别描述了任务竞争处理器资源的能力和持有处理器时间长短的能力。这两者同时是任务抢占的重要参数。因任务时间片运行完毕而引起的任务调度可以理解为时间片调度,而因为内核中最高就绪优先级的变化而引起的调度则为优先级调度。

任务调度和调度方式

任务调度是内核的主要功能之一,在任务需要调度的时候,内核会根据具体的调度算法和策略选择合适的任务,替换当前任务占有处理器等硬件资源。根据调度原理的不同,任务调度方式可分为可抢占调度和不可抢占式调度两类。

基于优先级的可抢占式调度的实时性好,任何优先级高的任务只要具备了运行的条件,即进入了就绪态,就可以立即得到调度和运行。任务在运行过程中都随时可能被比任务在运行过程中都随时可能被比=它优先级高的任务抢占。这种方式的任务调度保证了系统的实时性。

任务调度算法

常见的任务调度机制主要有时间片轮转调度算法(时分式)、优先级调度算法(抢占式)和基于优先级的时间片调度算法。

基于优先级的时间片调度算法吸收了前两种算法的优点,同时又解决了它们的不足。这种算法为每个任务都安排了优先级和时间片。在不同优先级的任务间采用优先级调度算法,在相同优先级的任务间使用时间片轮转调度算法。任务调度策略首先考虑任务的优先级,优先级高的任务必定会抢占低优先级的任务。相同优先级的任务则按照时间片长度比例共享处理器时间。这样既保证了能够尽快响应紧急任务,又保证相优先级的任务都有机会轮流占有处理器。

基于优先级的时间片调度算法

如上图所示

  • 任务 1 优先级为 3,时间片长度为 2 个时钟节拍;任务 2 的优先级为 3,时间片长度为 3;任务 3 优先级为 5,时间片长度为 1 个时钟节拍;任务 4 优先级为 1,时间片长度为 2 个时钟节拍;
  • T0 时刻,系统中只有任务 4 就绪,所以任务 4 得到处理器,运行;
  • Ta 时刻,任务 2 和任务 1 就绪,任务 2 因优先级抢占得到处理器,开始运行;
  • T2 时刻,任务 2 时间片耗尽,只好释放处理器;任务 1 得到运行的机会;
  • T3 时刻,任务 1 时间片耗尽;任务 2 得到运行的机会;
  • Tb 时刻,任务 3 就绪,发生优先级抢占;任务 3 得到运行的机会;
  • Tc 时刻,任务 3 处理完自己的工作,主动释放处理器;任务 2 得到运行的机会;
  • Td 时刻,任务 2 处理完自己的工作,主动释放处理器;任务 1 得到运行的机会;
  • Te 时刻,任务 1 处理完自己的工作,主动释放处理器;任务 4 得到运行的机会;
同步、互斥和通讯

在多任务系统中,在任务间、ISR 和任务间必然存在着处理器交替抢占,轮流执行的情况。除此之外,这些可执行对象也存在着其它关系,仔细观察这些对象,它们总是要“走走停停、互相照应”,这也正是多任务系统的特点,只有这样设计系统才能使得硬件资源得到最大的利用。可以把它们间的关系总结如下:

  • 共享资源的竞争
    • 任务或者 ISR 访问共享资源时是互相竞争的,只能被一个任务或者 ISR 访问,并且操作时不能被打断。强调的是“互斥”的概念。
  • 运行同步
    • 任务间或者任务和 ISR 间互相协作,按照规定的路线执行,也就是对它们的执行步骤和顺序有要求。强调的是“同步”的概念。同步可以是单向的也可以是双向的。
  • 数据通信
    • 任务间或者任务和 ISR 间的数据传输,常见的模式是一方提供数据,另一方处理数据,共同完成某些功能。强调的是“通信”的概念。
    • 任务间的数据传输,可以是直接的,也可以是间接的。
      • 在直接数据传输方式下,一个任务可以把数据直接发给指定的任务,发送过程很明确的说明了哪个任务把数据传给了那个任务。
      • 间接方式指的是数据交互的双方,约定一个数据缓冲区,发送数据的任务首先会把数据发往该缓冲区,然后通知接收数据的任务则从该缓冲区取得数据。从内核角度来考虑,内核不关心这些数据的含意,只当普通的数据来处理。
中断机制

中断机制是处理器的重要基础设施。它用来对各种事件的响应和处理。当外设或者处理器自身有事件发生时,处理器会暂停执行当前的代码,并转向处理这些中断事务。在处理器与外设间的交互大多采用中断来完成,中断系统能极大提高系统的效率。

发出中断请求的来源叫做中断源。根据中断源的不同,可以把中断分为三类:外部中断、内部中断、软件中断。

机器学习算法


  1. AI嵌入式系统:算法优化与实现 ↩︎

  2. 嵌入式实时操作系统原理与最佳实践 ↩︎

  3. 环形缓冲区 ↩︎

  4. C语言实现环形缓冲区 ↩︎

  5. 关于时钟周期、状态周期、机器周期、指令周期的解释 ↩︎

  6. 线程和进程的区别是什么? ↩︎