跳转到主要内容

后端通信

在大多数应用中,UI需以某种方式连接到系统的其余部分,并发送和接收数据。 它可能会与硬件外设(传感器数据、模数转换和串行通信等)或其他软件模块进行交互通讯。

本文描述了实现此类交互通讯的推荐解决方案。

第一种方法是一种“快而不精”的方法,主要用于原型开发,而第二种方法是一种在架构上较完善的方法,可正确地连接UI与现实世界中的剩余控件。

在本文末尾,我们会介绍使用两种方法的示例链接。

Model类

所有TouchGFX应用都有Model类,Model类除了存储UI状态信息,还可用作面向周围系统的接口。 这里周围系统我们指的是在你整个系统中用到的硬件外设以及需要进行通讯的其他任务。 通常来讲,在各自的View类中直接访问其他软件模块或者硬件外设并不是个好的设计。

Further reading
如需了解更多关于Model的知识,请参考:MVP模式

Model类非常适合放置任何此类接口代码,原因在于:

  1. Model类有 tick() 函数,会在每一帧自动调用,并且可实现用于查找来自其他子模块的事件或对事件作出反应。
  2. Model类有一个指向当前活动Presenter的指针,它能够将传入事件通知给UI。

系统接口

与周围系统交互通讯的方式有两种:一种是从GUI任务直接采样,另一种是从另外一个任务采样。

从GUI任务采样

与周围系统交互通讯的最佳方式取决于您需要的采样频率、采样时间消耗和时间的严格性要求。

如果这些方面的要求并不严格,那么最简单的方法就是在 Model::tick 函数中直接对周围系统进行采样。

如果采样频率低于帧率(通常约为60Hz),您可以延长计数周期,例如每次只在第N个计数点采样。 如果这样做,那么采样操作必须稍微快一些(通常为1ms或更短),否则会影响帧率。因为采样是在GUI任务里执行的,这样会延迟帧绘制。

从其他任务采样

如果不方便将与周围系统交互通讯的采样放在GUI任务里,那么可以新建负责执行采样操作的OS任务。

您可以根据特定场景的需要,将该任务配置为以准确的时间间隔运行。 此外,根据您的需求,此新任务的优先级可以低于或高于GUI任务。

如果优先级更高,无论GUI任务在执行什么操作,都能确保它会在指定时间准确运行。 有一个缺点是,如果是CPU占用进程时间长,可能会影响UI的帧率。

另一方面,如果采样对时间的要求不严格,则可以分配低于GUI任务的优先级,这样UI帧率将永远不受周围系统采样的影响。 在渲染时,GUI任务将休眠很长时间(如在等待基于DMA的像素传输完成时),这可允许优先级较低的任务频繁运行,对绝大多数应用而言也足够了。

如果您使用其他任务,建议您使用RTOS提供的任务间消息传送方法。 大多数RTOS具有队列/邮件机制,可从一个任务向另一个任务发送数据(通常为用户定义的C语言结构体、字节阵列或简单的整数)。 为了将新数据传递给GUI任务,需要为UI任务设置邮箱或消息队列,并使用此消息传送系统将数据发送给GUI任务。 然后可以在 Model::tick 中轮询GUI任务的邮箱,查看是否有任何新数据到达。 如果有,读取数据并相应地更新UI。

向UI传输数据

无论您是从GUI任务采样还是从二级任务采样Model::tick 函数都是GUI任务发现要在UI中显示的新数据的地方。 除了充当周围系统的接口,Model类还负责保存状态数据(如前文所述),因此可能有些状态变量也需要更新。

我们来考虑一个简单的示例:温度传感器连接到系统,将当前温度显示在UI上。 在准备阶段中,我们在Model类新增函数以便支持:

Model.hpp
class Model
{
public:
// Function that allow your Presenters to read current temperature.
int getCurrentTemperature() const { return currentTemperature; }

// Called automatically by framework every tick.
void tick();
...
private:
// Variable storing last received temperature;
int currentTemperature;
...
};

在上述情况下, Presenter 能够询问Model当前温度,以便在进入显示温度的屏幕时Presenter在UI(View)中设置此值。 现在,需要能够在接收到新的温度信息时再次更新UI。 为此,我们利用Model有指向当前活动Presenter的指针。 该指针的类型是接口(ModelListener),为了反映合适的应用特定的事件,您可以进行修改:

ModelListener.hpp
class ModelListener
{
public:
// Call this function to notify that temperature has changed.
// Per default, use an empty implementation so that only those
// Presenters interested in this specific event need to
// override this function.
virtual void notifyTemperatureChanged(int newTemperature) {}
};

现在,我们已经连接了此接口,剩余的工作是执行传入“新温度”事件的实际采样。 Model::tick

Model.cpp
void Model::tick()
{
// Pseudo-code for sampling data
if (OS_Poll(GuiTaskMBox))
{
// Here we assume that you have defined a "Message" struct containing type and data,
// along with some event definitions.
struct Message msg = OS_Read(GuiTaskMBox);
if (msg.eventType == EVT_TEMP_CHANGED)
{
// We received information that temperature has changed.
// First, update Model state variable
currentTemperature = msg.data;

// Second, notify the currently active Presenter that temperature has changed.
// The modelListener pointer points to the currently active Presenter.
if (modelListener != 0)
{
modelListener->notifyTemperatureChanged(currentTemperature);
}
}
}
}

以上方法可确保两点:

  1. currentTemperature 变量总是最新值,因此Presenter可随时获取当前温度。
  2. Presenter 立即通知温度变化,并能采取合适措施。

MVP模式的一大优势是根据当前所在屏幕实现单独的通知处理。 例如,假设在显示某些设置菜单(例如MainMenuPresenter/MainMenuView是激活的) 时发生了一个温度变化的事件, 而当前温度与此无关。

因为 notifyTemperatureChanged 函数具有默认的空实现,此通知会被 MainMenuPresenter忽略。. 另一方面,如果 有TemperatureControlPresenter ,您可以在该Presenter中重写 notifyTemperatureChanged notifyTemperatureChanged函数,并通知View它应显示更新后的温度:

TemperatureControlPresenter.hpp
class TemperatureControlPresenter : public ModelListener
{
public:
// override the empty function.
virtual void notifyTemperatureChanged(int newTemperature) {
view.setTemp(newTemperature);
}
};

当然,View类 TemperatureControlView必须实现 setTemp 方法。

UI向周围系统发送数据

对应的从UI向周围系统传输数据/事件时,将通过Model以大体上相同的方式来执行。 继续前面的例子,如果我们需要增加设置新的目标温度的能力,我们将向Model添加以下内容:

Model.hpp
void setNewTargetTemperature(int newTargetTemp)
{
// Pseudo-code for sending an event to a task responsible for controlling temperature.
struct Message msg;
msg.eventType = EVT_SET_TARGET_TEMP;
msg.data = newTargetTemp;
OS_Send(SystemTaskMBox, &msg);
}

如果用户在UI中设置新的目标温度,View可通知保有指向Model对象指针的Presenter,从而能够调用 setNewTargetTemperature函数。 函数中直接对周围系统进行采样。

示例

下面的示例是为特定于板件的演示(BSD),但演示的许多代码可重复用于其他演示板和定制硬件。 对于这些示例,我们在STM32CubeMX中创建任务和队列。 我们然后填充生成的任务,并在 main_user.c中实现示例用户代码。. 示例使用STM32CubeMX BSP库来控制STM32评估套件上的LED、用户按钮、以及其他外设。

来自GUI任务

一个示例应用、一个BSD,位于最新版本的TouchGFX设计器下,具体路径> 演示->板件特定演示> STM32F46G探索套件控制LED(从GUI)。

该应用演示如何对按钮进行采样和控制LED。 Model类对按钮采样,并更新LED以与应用状态相匹配。

来自其他任务

一个示例应用、一个BSD,位于最新版本的TouchGFX设计器下,具体路径:演示-> 板件特定演示-> STM32H7B3I评估板模拟采样器任务。

该应用演示如何在单独的线程中对模拟输入进行采用。 该示例使用MVP架构将模拟值传输到View。

一个示例应用、一个BSD,位于最新版本的TouchGFX设计器下,具体路径:演示-> 板件特定演示-> STM32F46G探索套件任务间通信。

该应用演示了任务间通信,以及与UI之间的传播。 在您自己进行设置时,它也许能激发您的灵感。 该示例在后端系统使用C代码实现,在TouchGFX GUI端使用C++实现,后端与TouchGFX之间相互通讯。 该示例可在包含FreeRTOS操作系统的STM32F746G-DISCO板上运行。

来自多个任务

一个示例应用、一个BSD,位于最新版本的TouchGFX设计器下,具体路径:演示->板件特定演示-> STM32F769I探索套件多任务通信演示> 。

该应用对按钮状态进行采样,并在按下按钮时通过GUI消息队列传递消息。 因此我们可以通过按住按钮来推进应用中的动画。

该应用使用了3个FreeRTOS任务。 一个用于GUI,另外两个分别用于两个外设(LED和用户按钮)。

来自任务和外部中断

一个示例应用、一个BSD,位于最新版本的TouchGFX设计器下,具体路径:演示-> 板件特定演示-> STM32F769I探索套件多任务通信演示> 。

该应用专为STM32F769I-DISCO板而设计,它与LED和用户按钮交互,以便展示如何将C代码和硬件外设集成到TouchGFX应用中。

该应用以EXTI模式配置按钮(外部中断线路0)。 其行为是在按下按钮时接收中断,此后中断清零。 这不允许出现与GPIO模式下相同的行为,而会是单步动画,原因在于只在接收到中断时通过GUI消息队列发送消息。

该应用使用两个FreeRTOS任务。 一个用于GUI,一个用于LED。 (多任务演示中的Button任务在该应用中仍处于活动状态,外设交互代码已移至中断处理函数中)。