跳转到主要内容

操作系统

引言

本节将讨论图形用户界面应用中操作系统的使用。

嵌入式设备越来越先进。 系统的大部分设备不仅处理图形用户界面,通常还处理复杂的控制算法和任务。

举例来说,这些任务可以是电机控制、数据获取或安全相关任务。 许多先进设备包含用来与数据中心通信的通信协议栈(如TCP/IP)或用来与其他本地设备通信的射频协议栈(如蓝牙)。

其他任务与用户界面的交互

在具有图形用户界面并只支持几项简单任务的简单设备中,可围绕用户界面代码构建整个应用。 除了常规的用户界面升级,应用执行的任务非常少,因此可将其他任务的执行相当成功地嵌入用户界面代码。

当设备包含具有独享时序要求的更高级的“后台运行”功能(如调制电机)时,将很难将这两个任务合二为一。

正如我们在之前的文章中讨论的那样,图形引擎必须持续绘制新帧,才能支持流畅的用户界面。 如果在运行其他任务时需要暂停图形引擎任务的运行,帧率将会下降。 同样地,如果其他任务只在帧间、空闲时间运行,那么在用户界面渲染复杂场景时,由于空闲时间较少,这些任务会受到影响。 这些影响使得UI任务与其他复杂任务的手动交替变得困难。

示例

在本节剩余部分,我们将构建一个具有显示屏的蓝牙扬声器。 我们有3个主要任务:运行图形用户界面,将音乐输入扬声器,以及处理蓝牙栈以便与其他设备通信。

不难看出,以用户界面为中心的应用架构并不是好的选择:假设我们将音乐代码与用户界面混合,并将启动回放的代码放在用户界面上某个按钮的事件处理中。 现在,需要一点时间才能开始播放音乐,期间用户界面被锁定。 与此同时,运行的任何动画都将停止。

一般情况下,用户界面的响应性开始依赖于音乐任务的执行时间(开始、停止和下一首等)。 这是一个笼统的问题,我们稍后再做讨论。

如果还想从蓝牙播放音乐,会发生什么? 用户界面是否应以某种方式介入其中?

我们如何为音乐任务分配优先级,以避免音乐暂停? 与此同时,我们还希望在没有音乐任务运行时用户界面以最高性能运行。

操作系统可通过任务、通信手段和同步来解决所有这些问题。

RTOS

实时操作系统是一个小软件,它通过各种服务为应用提供支持,并为应用中的任务分配计算资源。

RTOS帮助您在许多独立但相互协作的任务中构建应用。 然后,在要用到这些任务时,RTOS会根据任务的优先级并发执行这些任务。

我们甚至可以将一项作业分割成一个高优先级任务和一个低优先级任务。 假设我们必须在蓝牙数据到达时非常快速地从缓冲区读取数据,并将它们放入较大的应用缓冲区。 数据处理可能会稍微延迟。 这样一来,我们将有两个蓝牙任务。

在本例中,我们从主函数开始4项任务:

int main() {
...
os_start_task(gui_task, medium_priority);
os_start_task(music_task, low_priority);
os_start_task(bt_comm_task, high_priority);
os_start_task(bt_appl_task, low_priority);
os_start_scheduler();
}

同样可以将音乐的任务分为两个: 一个高优先级任务将数据输送到扬声器,一个低优先级任务控制播放哪首歌曲 并向用户界面发送通知。

使用上述不同优先级的结果是:当有数据要处理时,运行bt_comm_task任务;否则,运行用户界面任务。 当用户界面任务等待显示屏时,两个低优先级任务可以运行。 操作系统调度程序将为我们处理此类时间分配。

在典型的TouchGFX应用中,用户界面在每一帧中等待显示屏,它还定期等待 图形加速器ChromArt,以便完成绘制元素。 这意味着高优先任务会有许多短暂的暂停,优先级较低的任务可以在暂停期间运行。 操作系统调度程序将自动更改MCU,以便在优先级较高的任务等待时运行这些任务。

Task communication

当我们使用多个任务时,还需要一种安全的通信方式用于任务间的通信。 举个简单的例子,从用户界面到音乐任务。 除其他情况外,这里我们需要音乐任务进行等待,直至gui_task任务要求其开始播放歌曲。 一种简单的实现方式是使用消息队列。 在队列中出现消息之前,音乐任务休眠。 当队列中出现消息时以及优先级较高的任务不忙碌时,调度程序 唤醒任务。

   ...
music_task_input_queue = os_create_queue(10); //10 element queue
...

在用户界面中,当按下“播放”时,我们向音乐任务的队列发送一条消息:

void ScreenMusic::handlePlayPressed()
{
os_send_message(music_task_input_queue, play_message);
}

音乐任务能够以读取队列的方式等待消息。 这会在有消息到达前阻塞任务:

...
Message message;
os_receive_message(music_task_input_queue, &message);

在将消息放入音乐任务的队列后,用户界面继续运行并尽快渲染帧。 我们不将时间浪费在立即处理播放消息上。 但是,在渲染完成后,在渲染下一帧之前UI任务处于等待状态,调度程序转去执行音乐任务,此任务将处理传入消息。

同样地,我们还可以给予用户界面一个输入队列。 然后,音乐任务可以发送通知消息,例如在歌曲结束时。 用户界面任务不应等待消息,而应快速检查是否有消息(无阻塞)并读取它(如果有)。

此设置在系统中的任务之间提供了一种十分灵活的连接。 我们实际上无需使用用户界面即可测试音乐任务,我们还可以轻松地从蓝牙任务播放音乐。

处理中断

一些任务需要作为对中断的响应来运行。 在我们的示例中,蓝牙通信任务就是这样一个例子。 我们希望任务在蓝牙芯片有新的数据包时运行。 假设我们能够在那种情况下产生一个中断,我们就可以从中断处理器发送一条消息:

void BT_DataAvailable_Handler(void)
{
os_send_message(bt_data_queue, data_available_message);
}

除了队列,还有其他同步的策略可用。 例如,许多操作系统中都有信号量和互斥量。

FreeRTOS

在开发期间,TouchGFX的测试是基于FreeRTOS操作系统的。 TouchGFX对系统的要求非常少并能在许多其他操作系统上运行,但FreeRTOS是一个很好上手的操作系统,除非您有其它特别的要求。

FreeRTOS是一种简单的操作系统,在商业应用中可免费使用。 它以源代码的形式随STM32Cube固件一起供应,为所有STM32微控制器提供了可直接使用的示例。

请访问freertos.org了解FreeRTOS的更多信息和许可条款。

TouchGFX OS Wrappers

TouchGFX默认配置是在FreeRTOS上运行,使用一个消息队列实现与显示屏控制器的同步,并使用信号量保护对帧缓冲的访问。

这是通过在touchgfx/os/OSWrappers.cpp中定义的OSWrappers类来处理的。 这个类具有下列方法:

方法说明
signalVSync()应在显示屏为下一帧做好准备时从显示屏驱动调用此方法。
waitForVSync()由图形引擎调用用于等待。 在signalVSync被调用前不应返回。
isVSyncAvailable()(可选)如果发生了VSync,则返回true。 在waitForVSync中可用于避免阻塞。
signalRenderingDone()(可选)删除任何未处理的VSync信号。
takeFrameBufferSemaphore()由图形引擎和加速器调用,用来获取对帧缓冲的直接访问
giveFrameBufferSemaphore()用于再次释放直接访问时被调用。

默认实现是使用消息队列实现VSync(帧)同步。 在下一个VSync到达前,图形引擎任务休眠。

此OSWrapper类由TouchGFX Generator生成。 阅读关于Generator的更多内容,点击 链接

无RTOS

TouchGFX还可以在没有操作系统的情况下运行。 在这种情况下,必须在主函数中直接开始图形引擎主循环:

int main()
{
...
touchgfx::HAL::getInstance()->taskEntry();

//never returns
}

不使用RTOS并不会降低TouchGFX的性能。 可能会增加MCU负载,并增加与TouchGFX一起运行其他任务的难度。

如上文所述,现在您需要在主函数中用户界面运行时手动控制其他的任务。

Model::tick

一种方式是在每一帧中的Model类中检查所有的任务:

Model.cpp
void Model::tick()
{
//run other tasks here
music_task_tick();
bluetooth_task_tick();
}

使用此方法可在每一帧将所有任务执行一次。 任务消耗的时间会被添加到用户界面的渲染时间中去。 对于所有任务均可快速结束的简单系统而言,这是一种简单且可接受的解决方案。

OSWrappers

另一种方法是在OSWrappers类中使用钩子函数。 如上文所述,图形引擎在需要等待事件时调用此类上的方法。 您可以在等待所述事件时使用此方法执行其他工作:

OSWrappers.cpp
static volatile uint8_t vsync_sem = 0;

void OSWrappers::signalVSync()
{
vsync_sem = 1;
}

void OSWrappers::waitForVSync()
{
vsync_sem = 0; //clear the flag, so we wait for the next vsync
do {
// Perform other work while waiting
music_task_tick();
bluetooth_task_tick();
} while(!vsync_sem);
}

使用此方法时,其他任务可以充分使用帧间的空闲任务,但任务可用的时间会不一样。

另一种解决方案是使用OSWrappers::isVSyncAvailable和OSWrappers::signalRenderingDone函数。 这将帮助应用避免拥有多个while循环。 当选择无操作系统配置时,TouchGFXGenerator将使用这些函数。

任务必须能够将其工作分割成时长大概1毫秒的小步骤。 否则,将影响用户界面性能。