谷动谷力

 找回密码
 立即注册
查看: 1452|回复: 0
打印 上一主题 下一主题
收起左侧

【项目分享】采用 STM32H750 探索套件的新能源监控系统 UI

[复制链接]
跳转到指定楼层
楼主
发表于 2023-1-6 10:44:31 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

【项目分享】采用 STM32H750 探索套件的新能源监控系统 UI

本项目旨在使用 STM32H750-DK 开发板基于 TouchGFX 库设计一套城市新能源监控系统的 UI 界面,分为欢迎页和三大不同的能源版本主页,通过GUI的功能实现,可以呈现高端应用的酷炫交互界面。
▲ STM32H750 探索套件全家福
在详述实现过程之前,我们先看一下 ST 专家点评。




ST专家点评





从这个评测贴中我们可以看到这位同学给我们展示了如何使用STM32H750+TouchGFX开发平台快速开发一个使用新能源监控系统的应用。从UI设计的角度来看:应用包含了以下几个页面(启动界面/主界面/供水界面/风能界面/充电管理), 涵盖了常见的开机启动、数据/曲线展示/充电引导和控制的功能;UI设计使用TouchGFX Designer自带的基础控件图片/文本控件进行了基础布局,动态图片/staticGraph控件用于充电动画/图表等功能;通过本评测作品,我们可以看到,通过STM32H7 MCU上添加免费的TouchGFX中间件,就可以快速在MCU上添加UI交互功能, 而不需要额外添加HMI显示模组。从整体上来看:在新能源汽车和服务也在走进我们的生活的背景下,其交互体验也在向智能手机方向发展,显示美观、交互友好的方向发展。这个设计作品可以给我们在未来新能源相关设备端产品人机交互设计方面提供了一个很好的设计参考。
一、搭建MDK5工程1.1开发环境
开发板:STM32H750B-DK
TouchGFX软件版本:V4.20.0MDK5软件版本:V5.33.0STM32CubeMX软件版本:V6.6.1
操作系统:Windows10
1、下载官方demo
为了快速GUI开发,使用TouchGFX基于STM32H750B-DK的工程模板。打开TouchGFX,在搜索框中输入STM32H750B-DK,点击Create按钮,创建工程。
TouchGFX生成工程结构如下:
生成的文件夹里面有一个STM32CubeMX的工程文件,由于默认配置中生成的是STM32CubeIDE工程代码,我们需要的是keil工程代码,所以需要切换IDE(除了这点,不改任何配置)。
此时,keil工程模板基本建立,但此时如果直接打开keil工程,编译会报错,是由于TouchGFX没有生成代码,导致确少界面相关源代码,只需要回到TouchGFX,点击产生代码。
再次编译keil工程,无报错,如果有警告,再次编译后可消失,生成.hex文件。
至此,基于STM32H750B-DK的MDK工程模板创建完成。
二、界面设计
1、启动界面

圆形进度条:制作两张加载图片,一张为背景,另一张为指示进度,在定时中断函数handleTickEvent()中计数,模拟上电启动加载的动态过程,实际上启动很快,此处故意加了一个延迟。相关代码如下:

class CarScreenView : public CarScreenViewBase
{
public:
  CarScreenView();
  virtual ~CarScreenView() {}
  virtual void setupScreen();
  virtual void tearDownScreen();
  virtual void clickButtonPressed();
  virtual void handleTickEvent();

protected:

  int tickCounter;
  int progress;
  int start;
};

#include <gui/startscreen_screen/StartScreenView.hpp>
#include <gui/mainscreen_screen/MainScreenView.hpp>

StartScreenView::StartScreenView()
{

}

void StartScreenView::setupScreen()
{
  StartScreenViewBase::setupScreen();
  tickCounter = 0;
  circleProgress.getRange(circleProgressMin, circleProgressMax);   
}

void StartScreenView::tearDownScreen()
{
  StartScreenViewBase::tearDownScreen();
}

void StartScreenView::updateProgress(uint16_t tick)
{
  circleProgress.setValue(tick % (circleProgressMax + 1));
}

void StartScreenView::updateDirection(uint16_t tick)
{
  if (tick % (circleProgressMax + 1) == 0)
  {
    application().gotoMainScreenScreenBlockTransition();
  }
}

void StartScreenView::handleTickEvent()
{

  tickCounter++;
  updateProgress(tickCounter);
  updateDirection(tickCounter);
}

2、主界面
按钮:是一种感应触控事件的控件,能够在按钮被释放时发送回调。每种状态(按下和释放)都关联了图像。添加按下和放开图片。交互:用来设置触发条件满足时要执行的动作。触发条件为按钮点击,动作为更换屏幕,并选择屏幕的过渡效果。
3、无负压供水
线条:一个基于画布控件的控件,能够绘制从一个点到另一个点的直线。本例通过函数  line.setStart(startX, startY)、 line.setEnd(endX, endY)改变线条的起始坐标,进而动态的改变线条的长短和位置,模拟水流的动画效果。主要实现代码如下:

void WaterSupplyScreenView::drawHorizontalLine(Line &line,uint16_t length)
{
  int startX, startY,endX, endY;

  line.invalidate();
  line.getStart(startX, startY);
  line.getEnd(endX, endY);
  if (endX < length)
  {
    if((endX - startX) < 10)
    {
      line.setStart(startX, startY);
      line.setEnd(endX + 1, endY);
    }
    else
    {
      line.setStart(startX + 1, startY);
      line.setEnd(endX + 1, endY);
    }   
  }
  else
  {
    if(startX < length)
    {
      line.setStart(startX + 1, startY);
      line.setEnd(endX, endY);      
    }
    else
    {
      line.setStart(0, startY);
      line.setEnd(0, endY);        
    }
  }
  line.invalidate();
}

void WaterSupplyScreenView::drawVerticalLineDown(Line &line,uint16_t length)
{
  int startX, startY,endX, endY;

  line.invalidate();
  line.getStart(startX, startY);
  line.getEnd(endX, endY);
  if (endY < length)
  {
    if((endY - startY) < 10)
    {
      line.setStart(startX, startY);
      line.setEnd(endX, endY + 1);
    }
    else
    {
      line.setStart(startX, startY + 1);
      line.setEnd(endX, endY + 1);
    }   
  }
  else
  {
    if(startY < length)
    {
      line.setStart(startX, startY + 1);
      line.setEnd(endX, endY);      
    }
    else
    {
      line.setStart(startX, 0);
      line.setEnd(endX, 0);        
    }
  }
  line.invalidate();
}

void WaterSupplyScreenView::drawVerticalLineUp(Line &line,int length)
{
  int startX, startY,endX, endY;

  line.invalidate();
  line.getStart(startX, startY);
  line.getEnd(endX, endY);

  if (endY > 0)
  {
    if((startY - endY) < 10)
    {
      line.setStart(startX, startY);
      line.setEnd(endX, endY - 1);
    }
    else
    {
      line.setStart(startX, startY - 1);
      line.setEnd(endX, endY - 1);
    }   
  }
  else
  {
    if(startY == 0)
    {
      line.setStart(startX, length);
      line.setEnd(endX, length);        
    }
    else if(startY <= 10)
    {
      line.setStart(startX, startY - 1);
      line.setEnd(endX, endY);      
    }   
  }
  line.invalidate();
}

void WaterSupplyScreenView::handleTickEvent()
{
  static double data = 1.0;

  tickCounter ++;
  if(tickCounter % 5 != 0)
    return;

  drawHorizontalLine(line1, 28);
  drawHorizontalLine(line2, 25);  
  drawHorizontalLine(line3, 90);  
  drawHorizontalLine(line4, 15);  
  drawHorizontalLine(line5, 15);  
  drawHorizontalLine(line6, 40);  
  drawHorizontalLine(line7, 15);  
  drawHorizontalLine(line8, 15);  
  drawHorizontalLine(line9, 15);  
  drawHorizontalLine(line10, 15);  
  drawHorizontalLine(line11, 20);  
  drawHorizontalLine(line12, 15);  
  drawHorizontalLine(line13, 15);  
  drawHorizontalLine(line14, 15);  
  drawHorizontalLine(line15, 15);  
  drawHorizontalLine(line16, 15);  
  drawHorizontalLine(line17, 15);  
  drawVerticalLineDown(line18, 30);   
  drawVerticalLineDown(line19, 30);   
  drawVerticalLineDown(line20, 65);   
  drawVerticalLineDown(line21, 30);   
  drawVerticalLineUp(line22, 110);   
  drawVerticalLineUp(line23, 30);   
  drawVerticalLineUp(line24, 80);  
  if(tickCounter > 50)
  {
    drawHorizontalLine(line25, 90);
    drawVerticalLineUp(line28, 110);   
    drawVerticalLineDown(line34, 65);
    drawVerticalLineUp(line37, 30);  
    drawHorizontalLine(line38, 40);  
    drawVerticalLineUp(line40, 80);  
    drawVerticalLineDown(line39, 30);  
  }
  if(tickCounter > 150)
  {
    drawHorizontalLine(line26, 90);
    drawVerticalLineUp(line29, 110);
    drawVerticalLineDown(line35, 65);
    drawVerticalLineUp(line41, 80);      
  }
  if(tickCounter > 250)
  {
    drawHorizontalLine(line27, 90);  
    drawVerticalLineUp(line30, 110);  
    drawVerticalLineDown(line36, 65);
    drawVerticalLineUp(line42, 80);   
  }   
  if(tickCounter > 350)
  {
    drawHorizontalLine(line31, 90);  
    drawVerticalLineUp(line32, 110);
  }  
  if(tickCounter > 450)
  {
    drawVerticalLineUp(line33, 110);
  }  
  if(tickCounter % 50 == 0)
  {
    data += 0.1;
    if(data > 3.0)
    {
      data = 1.0;
    }
    Unicode::snprintfFloat(textCounter1Buffer, TEXTCOUNTER1_SIZE, "%0.1f", data);
    textCounter1.invalidate();  
    Unicode::snprintfFloat(textCounter2Buffer, TEXTCOUNTER2_SIZE, "%0.1f", data);
    textCounter2.invalidate();        
  }
}

4、风电监控系统
动画图像:添加一组同一标识符风车的图像,每个图像间隔的时长为100ms,从头至尾运行,模拟风车动画效果。
动态图表:应用程序在Y轴显示发电功率,X轴显示时间,实时显示风力发电的功率,动态图表支持三种类型的动态行为:换行并清除、滚动、换行并覆盖。本例选用滚动。
主要实现代码如下:

static uint16_t randomish(int32_t seed)
{
  static uint16_t last = 0;
  const uint16_t num = (seed * (1337 + last)) % 0xFFFF;
  last = num;
  return num;
}

void WindPowerScreenView::handleTickEvent()
{
  tickCounter++;
  if (tickCounter % 5 == 0)
  {
    float yMax = WindDynamicGraph.getGraphRangeYMaxAsFloat();
    int data = (int)((sinf(tickCounter * .02f) + 1) * (yMax / 2.2f)) + randomish(tickCounter) % (int)(yMax / 10.f);
    WindDynamicGraph.addDataPoint(data);
    Unicode::snprintf(powerBuffer, POWER_SIZE, "%d", data);
    power.invalidate();  
    Unicode::snprintf(WindSpeedBuffer, WINDSPEED_SIZE, "%d", data/5);
    WindSpeed.invalidate();        
  }
}
5、新能源汽车充电
动画图像:按钮按下后,从头至尾运行汽车动画,模拟充电,按下再次按下,动画停止显示。
文本进度条:显示充电进度,并且通过函数batteryTextProgress.setXY(x,y)改变文本的显示坐标。线性进度条:显示当前充电进度。主要实现代码如下:

void CarScreenView::handleTickEvent()
{
    if (CarAnimatedImage.isAnimatedImageRunning())   
    {
        tickCounter++;
        if (tickCounter > 1000)
        {
            CarAnimatedImage.stopAnimation();
            Unicode::snprintfFloat(moneyBuffer, MONEY_SIZE, "%0.1f", tickCounter/100);
            money.invalidate();   
            Unicode::snprintfFloat(KwhBuffer, KWH_SIZE, "%0.1f", tickCounter/100);
            Kwh.invalidate();   
            modalWindow.show();                    
        }
        else
        {
            batteryLineProgress.setValue(tickCounter/10);
            batteryTextProgress.setValue(tickCounter/10);
            batteryTextProgress.setXY((43 + (382-43)*tickCounter/10/100), 204);
        }      
    }     
}

模式窗口:是容器类型的控件,用于显示窗口,并禁止对下层视图和控件的触摸事件。充电完成或按键按下后,弹出窗口,显示充电费用和充电电量。
外部事件作为触发器:物理按钮作为触发器,应用程序响应外部按钮的输入,在TouchGFX/target 下创建KeyController.cpp,KeyController.hpp文件,编写按键初始化函数,以及按键采样函数,按键值设为‘1’,在电脑上模拟时按下数字键盘上的1,可模拟开发板上的物理按钮。主要实现代码如下:

#ifndef KEYCONTROLLER_HPP
#define KEYCONTROLLER_HPP
#include <platform/driver/button/ButtonController.hpp>
namespace touchgfx
{
        class KeyController : public ButtonController
        {
                public:
                virtual void init();
                virtual bool sample(uint8_t& key);
        };
}
#endif

#include <KeyController.hpp>
#include "stm32h7xx_hal.h"
using namespace touchgfx;

void KeyController::init()
{
    GPIO_InitTypeDef GPIO_Initure;

    __HAL_RCC_GPIOC_CLK_ENABLE();           //开启GPIOA时钟
    GPIO_Initure.Pin = GPIO_PIN_13;            //PC13
    GPIO_Initure.Mode = GPIO_MODE_INPUT;       //输入
    GPIO_Initure.Pull = GPIO_NOPULL;            //浮空
    GPIO_Initure.Speed = GPIO_SPEED_FREQ_LOW;   //低速
    HAL_GPIO_Init(GPIOC, &GPIO_Initure);
}

bool  KeyController::sample(uint8_t& key)
{
    static int flag=0;

    if (HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13) == GPIO_PIN_SET)
    {
        if(flag == 0)
        {
            key = '1';
            flag = 1;
            return true;
        }
    }
    else
    {
        flag = 0;
    }
    return false;
}
在TouchGFXHAL.cpp文件添加代码,调用初始化函数,注册按键。

void CarScreenView::clickButtonPressed()
{
  if(start == 0)
  {
    start = 1;
    tickCounter = 0;
    CarAnimatedImage.startAnimation(false, false, true);   
  }
  else
  {
    start = 0;
    CarAnimatedImage.stopAnimation();
    Unicode::snprintfFloat(moneyBuffer, MONEY_SIZE, "%0.1f", tickCounter/100);
    money.invalidate();  
    Unicode::snprintfFloat(KwhBuffer, KWH_SIZE, "%0.1f", tickCounter/100);
    Kwh.invalidate();
    modalWindow.show();      
  }
}


void CarScreenView::handleTickEvent()
{
  if (CarAnimatedImage.isAnimatedImageRunning())   
  {
    tickCounter++;
    if (tickCounter > 1000)
    {
      CarAnimatedImage.stopAnimation();
      Unicode::snprintfFloat(moneyBuffer, MONEY_SIZE, "%0.1f", tickCounter/100);
      money.invalidate();  
      Unicode::snprintfFloat(KwhBuffer, KWH_SIZE, "%0.1f", tickCounter/100);
      Kwh.invalidate();  
      modalWindow.show();           
    }
    else
    {
      batteryLineProgress.setValue(tickCounter/10);
      batteryTextProgress.setValue(tickCounter/10);
      batteryTextProgress.setXY((43 + (382-43)*tickCounter/10/100), 204);
    }   
}   
}

三、总结
TouchGFX 是一款可用于STM32微控制器及处理器的GUI框架,即可用于STM32高性能产品,也可用于资源有限的STM32MCU上,提供类似于手机应用界面般的酷炫显示效果。来源:STM32论坛网友yang377156216  版权归原作者所有
+10
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|深圳市光明谷科技有限公司|光明谷商城|Sunshine Silicon Corpporation ( 粤ICP备14060730号|Sitemap

GMT+8, 2024-12-29 23:51 , Processed in 0.095972 second(s), 43 queries .

Powered by Discuz! X3.2 Licensed

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表