本章介绍嵌入式软件编程模式和通用软件优化方案。嵌入式软件编程模式关注的是底层支撑软件的架构,包括内存和CPU运行时间的分配方案,它服务于上层机器学习软件。
嵌入式软件编程模式和传统的计算机编程模式在概念上有所不同,一般计算机软件编程模式侧重软件开发过程中的代码复用和架构标准化技术,以模块化和层次化的形式构建软件对象,而这一章所讨论的嵌入式编程模式是为了满足多任务的实时响应,在有限内存和有限运算能力的嵌入式CPU上高效地完成运算任务。本章讨论的内容除了编程模式外,还包括通用的嵌入式软件优化方案,它们和嵌入式软件编程模式相结合,保障了机器学习算法在嵌入式平台上高效运行。这一章的内容除了可用于提高机器学习应用软件效率外,也能够应用于其他具有类似特性的嵌入式软件。
嵌人式系统和应用紧密结合,不同应用有不同的软件运行要求。下面将讨论几种常见的嵌入式系统编程模式,每种模式有对应的应用场合,可以单独使用或者混合使用。这里讨论的编程模式主要针对没有操作系统的嵌入式软件运行环境,在这种情况下,CPU的全部算力可以分配到和应用相关的计算,不需要额外执行IO资源状态、内存清理、调度等软件操作系统的管理任务,因此运行效率和内存使用效率会更高,但付出的代价是需要手动管理任务并发、IO状态检查、资源共享等,对开发者有更高的要求。
下面首先给出常见的嵌入式系统运行模式示意图,如图2-1所示。
图中系统的功能是从传感器获得外部数据,进行分析运算后输出控制命令,运行期间还需要接收用户输入。
这一架构模型反映了大多数嵌入式系统的运行模式。比如,在语音识别应用中,嵌入式系统周期性地从ADC读入语音的波形数据,经过处理后识别用户语音指令,并根据识别结果输出控制命令。对于运行机器人视频避障软件的嵌入式系统,车软件需要通过摄像头周期性地获得视频数据,分析视频内容,识别障碍和目的地并输出机器人移动控制命令
上面描述的嵌人式系统运行模式涉及了多个需要并行执行的任务,每个任务有不同的运行周期性和处理优先级要求。表2-1给出了一个例子,其中嵌人式系统中需要同时运行4个任务,分别是传感器采样数据读取、数据计算及控制输出、用户输人命令检查、状态显示更新。它们的执行周期如表2-1所示。
这几个任务中,读入传感器数据的时间优先级最高,根据采样率所规定的时间间隔运行,不能错过数据;计算及控制输出同样有实时性要求,但它的执行频率低于数据采样;用户命令的检查和状态显示更新的优先级最低,只要满足人们对信息的感知速度即可。
上面几个任务的时间要求和运行频率要求也各不相同。设计时,需要根据优先级制定软件执行方案。当使用实时操作系统时,可以使用比如“单调速率调度算法”(Rate Monotonic Scheduling,RMS)等方案实现以上多任务并发执行,如图2-2示。我们后面讨论的几种编程模式能够不依赖于操作系统实现,允许开发者根据应用特点实现更灵活的定制化多任务执行模式。
周期运行模式的一种实现方式如代码清单2-1所示,该代码实现了表2-1给出的嵌入式系统的几个任务周期执行。其中while循环内部的几个任务按执行顺序排列,并且每轮while循环根据周期要求对各个任务执行1次或者多次。
whi1e循环内的代码对应所需要的任务的一个运行周期,如图2-2所示。但在实际情况中,由于每个模块运行时间存在差异,因此往往得到疏密不均匀的“周期”运行效果,如图2-3所示。这使得系统对传感器输入数据的“采样率”不再固定,并且对用户的输入响应间隔也时快时慢,系统运行缺少“确定性”。改进措施如下:
1)将运行时间过长的任务进行状态分割。
2)将采样固定在时间格点上运行。
下面详细介绍这两种改进方案。
运行时间长的任务拆分
任务拆分是将一个长时间任务分成几个阶段实现,每次循环执行时仅完成其中一个阶段的任务。例如,假设“状态显示更新”任务由于具有LCD特性而运行很慢,我们将其分成几部分,比如假设原始的操作可以分成三部分,如代码清单2-2所示。
该函数内部有static类型状态变量state,它的取值在函数一次调用完成到下一次调用期间是保持不变的。我们第一次调用该函数时,由于state=STAGEO,因此执行“更新测量数据曲线图”;第二次调用时,由于state=STAGE1,因此执行“更新数据计算结果”;第三次调用时,由于state=STAGE2,因此执行“更新用户输人内容”;三次调用后,state回到STAGEO,下次调用再次执行“更新测量数据曲线图”,并重复上面的过程,如图2-4所示。
通过上面的例子可以看到,我们能够将需要一次运行时间长的函数拆分成多个阶段实现,使得周期执行循环中,执行该函数的时间降低到各个阶段的执行时间。该模式适用于可以分解为多个执行阶段的函数,并且各个执行阶段相对独立,相邻执行阶段的运行时间可以不连续。
时间格点采样
对于传感器数据读取,当相邻两次调用读取数据的函数的间隔小于采样间隔时,通过代码清单2-4给出的伪代码实现采样时间调整,以确保每次数据读取在特定的时间格点上执行。
确定当前时间是否在采样时间格点上,如果不在,则延退等待固定时间。
值得注意的是,在很多情况下,采样时间间隔可以通过外部数据采样硬件确定,用户程序通过FIFO读取数据,当数据尚未到达时,FIFO为空,这使得我们可以基于FIFO是否为空来判断时间格点上的数据采样是否完成,如代码清单2-5所示。
上述模式只有当两次执行传感器采样数据读取”的间隔小于采样间隔时才有效,女如果大于采样间隔,则会导致采样点“遗漏如图2-5所示。
图2-5中虚线是采样时间格点,在中间由于“计算机控制输出”任务运行时间过长,出现过一次采样缺失现象。下面将讨论另外几种编程模式以避免这一问题。
前面基于循环调用的编程模式难以确保各个任务的等时间周期运行,并且把实时性要求高的任务和实时性要求低的任务混在一起,难以同时兼顾不同任务实时性要求的差异。为了能够确保特定任务的实时性,我们通常使用基于中断的前后台编程模式,如图2-6所示。
其中,时钟中断服务程序负责从外部传感器得到数据输入,由于时钟中断信号具有严格的周期性,因此可以确保数据间隔的稳定,并且由于中断服务程序随时能够打断前台的数据处理、用户输入、显示输出程序执行,因此不受它们的运行速度影响。
使用前后台编程模式时,需要考虑中断服务程序的运行效率,对于前面介绍的固定时间间隔的数据采样任务,要求中断服务程序能够在采样间隔内完成,因此需要尽可能地提高运行效率,在中断服务程序中只保留必不可少的代码,尽可能将那些对实时性要求不高的数据操作放到前台应用程序中执行。比如来自传感器的数据经过了压缩和纠错编码,在中断服务程序运行期间执行数据解压缩和纠错可能导致运行时间过长,进而导致错过下一个采样的时间点,为避免这一现象出现,可以将这些运行时间长但又没有必要在中断服务程序内完成的任务转移到“数据计算及控制输出”函数中执行。
使用前后台编程模式的一个极端示例是将所有操作移入中断服务程序。硬件提供不同重复间隔和优先级的时钟中断,这样主程序会很简单,只有一个无限循环,而所有的工作由中断服务程序完成,如图2-7所示。
基于中断服务程序实现所有操作的方案中需要编程者处理数据访问冲突问题,比如传感器数据读取中断服务程序有可能打断“数据计算及控制输出”中断服务程序,“改”还未处理完的数据,这就需要可靠的数据交换机制,以确保不同的代码能够安全地访问共享数据交换空间。比如通过图2-8所示的缓冲器交换数据。
“写指针”,而“数据计算及控制输出”程序负责从环形缓冲器读取数据,每次读完数据就修改缓冲器的“读指针”,避免对相同的共享数据区同时读写。
这一运行模式是基于中断的前后台运行模式的进一步改进。中断服务程序可以是传感器数据读取程序,也可以是用户指令输入检测程序。后台程序由一系列中断服务程序构成,负责读取数据供前台程序处理,还负责根据输入内容生成“事件”,将待处理事件加上数据序则不断检查事件队列,找到其中待处理的事件数据包,提取并执行,如图2-9所示。
事件队列可以以链表形式或者环形缓冲器形式实现,由于它是由中断服务程序和前台处理程序共享的,因此在编程时需要避免访问冲突。
基于事件列表运行模式的一种扩充是在系统中构建多个事件队列,每个事件队列具有对应的优先级,前台程序根据事件队列的优先级优先处理高优先级的事件,这样能够使对实时性要求高的事件(比如数据计算)能够及时得到处理,如图2-10所示。
这一编程模式在实际应用中要求程序员考虑“低优先级事件积压”问题,即高优先级事件不断出现,导致低优先级事件长时间得不到处理、处理延退过长的问题。需要程序员采取特定措施防止低优先级事件长时间得不到处理,一个解决方案是规定一个低优先级事件队列长度门限,当积压的低优先级事件数量超过门限时,临时提升该队列的处理优先级,避免低优先级事件队列内事件被过度积压。
对于需要在特定时间执行指定动作的应用,比如有特定时序要求的硬件设备控制和网络通信协议等,可以使用带时间信息的事件队列的设计模式(见图2-11)。这一设计模式和带优先级的事件队列运行模式相似,差别是这里的每个事件队列的优先级被执行时间所取代,系统根据当前时间检查事件队列,对于时间匹配的事件队列,执行队列中的所有事件,执行完删除该队列。
事件队列可以由当前正在执行的事件生成,也可以由中断服务程序生成。代码清单2-6中显示了这一模式的运行过程。
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删