跳转到主要内容

后端通信

在大多数应用中,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任务。 然后您可以使用Mode::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函数具有默认的空实现,此通知会被 ainMenuPresenter忽略。 另一方面,如果有TemperatureControlPresenter,您可以在该Presenter中重写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函数。

示例

下面的示例是为特定开发板配置的完整演示例程,但示例的许多代码可重复用于其他开发板和自定义硬件。

来自GUI任务

STM32F746的一个工作示例展示了如何在Model类中对按钮采样并直接控制LED。 该示例使用MVP架构实现两个视图和Model类之间传输值和事件。 Model类对按钮采样,并更新LED以与应用状态相匹配。

STM32F429的一个工作示例展示了如何在Model类中对按钮采样。 该示例使用MVP架构将按钮事件传输到View。

来自其他任务

STM32F469的一个工作示例展示了如何在单独线程中对模拟输入采样。 该示例使用MVP架构将模拟值传输到View。

还有一个工作示例展示了其他任务发送给以及来自UI的数据传输。 在您自己进行设置时,它也许能激发您的灵感。 该示例在后端系统使用C代码实现,在TouchGFX GUI端使用C++实现,后端与TouchGFX之间相互通讯。 该示例可在包含FreeRTOS操作系统的STM32F746G-DISCO板上运行。

来自多个任务

2018年5月28日举行的TouchGFX网络研讨会“与硬件集成”上演示了该工作示例

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

应用将按钮配置成GPIO模式。 在btntask.c中对按钮状态进行采样,并在按下按钮时通过GUI消息队列传递消息。 因此我们可以通过按住按钮来推进应用中的动画。

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

来自任务和外部中断

2018年5月28日举行的TouchGFX网络研讨会“与硬件集成”上演示了该工作示例

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

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

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