综述
本文将完成以下任务
开发一个温度传感器数据可视化应用,实时显示温度数据的图表。
STM32F4 内部集成温度传感器,并通过 ADC 获取温度数据。
在应用层中,ADC 采样值交由 TouchGFX 的 Model 处理,再由 Model 通过 MVP 架构传递给 Presenter,最后由 Presenter 更新 View,实现温度在界面上的实时显示。
开发工具
TouchGFX Designer 4.26.0,STM32CubeIDE 2.0.0,STM32CubeMX
MVP架构简介
在 TouchGFX 的 MVP 架构中,Model、Presenter 和 View 构成界面数据流的三层结构,各自承担不同职责。Model 负责维护应用层的数据状态,并从底层逻辑(如传感器采集、通信模块等)接收更新。当 Model 内部数据发生变化时,它不会直接操作界面,而是通过一个抽象接口 ModelListener 将更新通知上层。
ModelListener 是 Presenter 实现的回调接口,起到连接 Model 与 Presenter 的桥梁作用。通过该接口,Model 不需要关心当前显示的是哪个界面,也无需了解界面逻辑结构,从而实现彻底解耦**。
Presenter 作为中间层,一方面实现 ModelListener 以接收 Model 的数据更新;另一方面持有对 View 的引用,将处理后的数据传递给界面。Presenter 同时也是 View 的逻辑控制者,负责响应用户界面事件并协调 Model 的行为。
View 则专注于界面呈现,其职责仅包括根据 Presenter 提供的数据进行显示更新,不包含业务逻辑,也不直接访问 Model。
通过 Model → Presenter → View 的单向数据流,以及 ModelListener 的回调机制,TouchGFX 实现了结构清晰、层次分离、低耦合的界面框架,使界面渲染、界面逻辑与业务逻辑互不干扰。本项目应用的架构如下图所示。实现还是较为简单的。

温度图表界面的实现
UI绘制
Dynamic Graph 简介
Dynamic Graph通过固定长度的环形缓冲区存储点,每次加入新数据,当占据整个横坐标时自动滚动图线或重绘(可以在TouchGFX Designer中选择具体刷新方案),并在TouchGFX Designer中支持自定义数据范围、图线颜色、线宽、刷新方式和多曲线绘制,适合实时波形、传感器趋势等场景。
Dynamic Graph提供两个接口:
<控件名>.addDataPoint(0);//写入环形缓冲区
<控件名>.invalidate();//局部刷新,屏幕显示最新波形
具体可以参考官方文档,写的十分详细:动态图表 | TouchGFX Documentation
绘制步骤
-
首先新建一个Screen,重命名为TempScreen,设置为Startup Screen;同时打开Canvas Buffer,大小为默认的3600即可。
-
添加一个box并拉到最大,作为底图。

-
添加一个Dynamic Graph,重命名为tempGraph,位置和大小设置为W240 H260;Graph Area Margin 和 Graph Area Padding 分别是边距和填充区,具体可以参考下面的示意图;为了给坐标轴留足空间,Right和Bottom设置的稍微大一些,分别是:
Graph Area Margin
| Top |
Bottom |
Left |
Right |
| 10 |
15 |
20 |
10 |
Graph Area Padding
| Top |
Bottom |
Left |
Right |
| 10 |
15 |
20 |
10 |


-
接下来修改的是数据点的表达:
- Dynamic Behavior 动态行为(更新形式):
- 第一个是Wrap and Clear,是指当数据点达到横坐标轴最大值时,清空屏幕并重新从原点开始画(横坐标不会清零);
- 第二个是Scroll,顾名思义就是滚动显示;
- 第三个是Wrap and Overwrite,中间向右动态移动的竖线,将右边旧的数据更换成新的数据;
这里选择的是中间的Scroll,缺点是数据点设置得太多的话后面动态更新的时候会比较卡;后面如果测试的效果比较卡顿,可以将Data Points 修改为60甚至更低。
为了较为精确地显示芯片内部温度,精度等级设置为0.1。

-
接下来是图表的图线元素、网格线和坐标轴的设置:
- 首先是Elements,可以按照喜好加入圆点、方形、菱形、直线、面积等图元,这里只用Area和Line,其中Area的透明度(Alpha)调节到100左右;
- 然后是Grid Lines 网格,根据需要选择纵向的或者横向的;
- 最后是Labels 坐标轴,这边只修改了字体大小为10px;

-
放置一个Text Area,添加Wildcard通配符,修改名称为tempText,修改初始值为00.0,保证显示的内容的长度,并勾选上Buffer;
修改Translation中的文本为Temp:<value>°C;

来到Text->Typographies->你的TextArea设置的字体 下,在Wildcard Characters中加入1234567890.°,确保数字、小数点、℃能够正常显示。

-
完成以上内容后,点击右下角最左边的按钮,生成代码。

CubeMX配置
打开项目文件夹中的.ioc文件;

在Analog->ADC1->Mode里面找到Temperature Sensor Channel,勾选;

勾选后直接点击GENERATE CODE生成代码,可能会有以下弹窗,点击Yes即可。

生成完成后会有弹窗问你是否打开项目,点击Close或者直接关掉这个弹窗,手动打开CubeIDE工程。

CubeIDE代码编写
在<项目名>/CubeIDE中有.project和.cproject两个文件,双击打开任意一个。

在<项目名>/Core/Inc中新建一个文本文档,改名为temp_sensor.h,在<项目名>/Core/Src中新建一个文本文档,改名为temp_sensor.c;

将 temp_sensor.c 拖到/Application/User文件夹中,会弹出一个窗口,选择link to files;
temp_sensor.h不用动,因为/Core/Inc本就在工程的头文件路径里;

temp_sensor.h
主要声明1个函数:TS_GetTemperature(void),这一部分直接写在 main.h 和 main.c 也是可以的,单独新建文件只是为了项目结构更清晰。
#ifndef TEMP_SENSOR_H
#define TEMP_SENSOR_H
#include "main.h"
#ifdef __cplusplus
extern "C" {
#endif
float TS_GetTemperature(void);
#ifdef __cplusplus
}
#endif
#endif
temp_sensor.c
F429内部温度传感器的计算公式为:
$$
T(℃)={(Vsense - V25) /Avg_Slope}+25
$$
其中 Vsense是传感器读取到的电压值,V25 是 Vsense 在25℃时的典型值,Avg_Slope 是温度与 Vsense 曲线的平均斜率,典型值为2.5mV/℃。
#include "temp_sensor.h"
extern ADC_HandleTypeDef hadc1;
float TS_GetTemperature(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_480CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint16_t adcValue = HAL_ADC_GetValue(&hadc1);
float VSENSE = adcValue * 3.3f / 4095.0f; // 若 Vref=3.0 改成 3.0f,以此类推
float temp = ((VSENSE - 0.76f) / 0.0025f) + 25.0f;
return temp; // 单位 ℃
}
Model.hpp
主要在Model类的声明中加入了以下成员函数和变量的声明:
protected字段:
int tickCount = 0; //用于计数进入Model::tick的次数,如果采样频率太快可以启用
bool enableTemperatureSampling = false; //用于确定是否需要采样,当不在当前界面时停止采样,节省设备资源
public字段:
void setTemperatureSampling(bool enable) //提供给其他类的接口
{
enableTemperatureSampling = enable;
}
完整的类的声明如下:
class Model
{
public:
Model();
void bind(ModelListener* listener)
{
modelListener = listener;
}
void tick();
void setTemperatureSampling(bool enable)
{
enableTemperatureSampling = enable;
}
protected:
ModelListener* modelListener;
int tickCount = 0;
bool enableTemperatureSampling = false;
};
Model.cpp
需要包含"temp_sensor.h"头文件:
#include "temp_sensor.h"
主要修改Model::tick()函数,当温度界面激活时,在每个tick获取温度数据,并通过onNewTemperature()上传数据到View;
为了减少数据在层级之间传递的开销,将温度 t 乘以10后转化为int16_t类型的变量 t10 ,这样温度在Model->ModelListener->Presenter->View这一传输链路上都是以int16_t传递,减少性能上的浪费。
void Model::tick()
{
if (!enableTemperatureSampling){
return; // ← 关键:不在温度界面时不采样
}
tickCount++;
//if (tickCount >= 6)
//{
tickCount = 0;
float t = TS_GetTemperature(); // 调用 C 函数
int16_t t10 = (int16_t)(t * 10); // 放大 10 倍传给 GUI(避免用 float)
if (modelListener != 0)
{
modelListener->onNewTemperature(t10);
}
//}
}
ModelListener.hpp
在文件头加入#include <cstdint>,否则int16_t类型会报错;
在public中加入以下声明:
virtual void onNewTemperature(int16_t temp10) {};
因为此处声明的onNewTemperature()是虚函数,可以在后续的Presenter中提供具体实现。
TempScreenPresenter.hpp
在public中加入以下声明:
virtual void onNewTemperature(int16_t temp10);
TempScreenPresenter.hpp
Presenter主要做两件事:一是当窗口激活/失效时,分别使能/失能Model获取温度数据;二是当Model获取到数据时,通过view的updateTemperature()接口显示到图表和文本上,这个函数后面会实现;
void TempScreenPresenter::activate()
{
model->setTemperatureSampling(true);
}
void TempScreenPresenter::deactivate()
{
model->setTemperatureSampling(false);
}
void TempScreenPresenter::onNewTemperature(int16_t temp10)
{
view.updateTemperature(temp10);
}
TempScreenView.hpp
在public中声明:
void updateTemperature(int16_t temp10);
TempScreenView.cpp
包含头文件:#include <cstdint>
注意从Model传递来的是10倍温度的整型数据,为了正常显示,应该把它转化为浮点型:
void TempScreenView::updateTemperature(int16_t temp10)
{
// 1. 更新数字显示(tempText)
float t = temp10 / (10.0f);
Unicode::snprintfFloat(tempTextBuffer, TEMPTEXT_SIZE, "%.1f", t);
tempText.invalidate();
// 2. 更新折线图
tempGraph.addDataPoint(t);
tempGraph.invalidate();
}
下载验证
编写完成后通过CubeIDE编译并下载到开发板上:

代码已经放在Github上:STM32F429-TouchGFX/MyApplication_TemperatureMonitor at main · Chiando-1100/STM32F429-TouchGFX