单片机 GUI 设计(八)- 波形控件
单片机 GUI 设计(八)- 波形控件

单片机 GUI 设计(八)- 波形控件

基于 GD32F303ZET6 苹果派开发板

简介

做单片机 GUI 开发时,时常需要在屏幕上绘制波形。主流的绘制波形有两种方式,分别是滚动和扫描,本文将从这两个方面分别介绍。

以滚动的方式实现波形绘制

这种波形绘制方式与示波器类似,每增加一个波形点,就将整条波形整体往左边挪动一格,空出最右侧位置显示最新的波形点。因为每增加一个波形点,波形曲线就要整体往左边移动一格,所以消耗的 CPU 资源很多,容易造成卡顿,但显示效果好。以滚动方式实现波形绘制的效果如下所示。

控制结构体设计

波形绘制控件的控制结构体可以有如下定义。其中,pointNumCnt 用来记录波形点数量,每增加一个波形点就加一一次,最大值等于 width;waveBuf 是波形点缓冲区,用来记录波形点数据,该缓冲区由创建函数自动从系统堆区中分配,长度为 width。

typedef struct
{
  u16  x, y, width, height;  //区域
  u16  lineColor, backColor; //线条颜色和背景颜色
  u16  pointNumCnt;          //波形点数计数
  u16* waveBuf;              //波形点缓冲区,长度为波形控件的宽度,创建时由动态内存自动分配
}StructGraph;

API 函数声明

波形显示控件只需要两个 API 函数,分别是创建函数和添加波形点显示函数,如下所示。

void CreateGraph(StructGraph* widget);         //创建波形控件
void GraphAdd(StructGraph* widget, u16 value); //添加波形点

创建函数

创建函数的定义可如下所示,最主要的工作便是使用用户定义的背景色填充波形控件所在区域,以及为波形点缓冲区申请动态内存。为了加快内存拷贝速度,我们可以从单片机的内部 SRAM 为波形点缓冲区申请动态内存,内部 SRAM 比外拓 SRAM 读写速度快得多。最后别忘了清空波形点计数。

void CreateGraph(StructGraph* widget)
{
  u16 x0, y0, x1, y1;
  
  //计算波形显示区域
  x0 = widget->x;
  y0 = widget->y;
  x1 = widget->x + widget->width - 1;
  y1 = widget->y + widget->height - 1;
  
  //填充背景
  LCDFill(x0, y0, x1, y1, widget->backColor);
  
  //为波形点缓冲区申请动态内存
  widget->waveBuf = MyMalloc(SRAMIN, widget->width * sizeof(u16));
  if(NULL == widget->waveBuf)
  {
    printf("Fail to malloc for wave buf\r\n");
    while(1){}
  }
  
  //清空计数
  widget->pointNumCnt = 0;
}

添加波形点函数

用户向波形控件添加一个波形点时,有两种情况。一是波形点缓冲区未满,波形还未从左边绘制到右边,此时可以简单的将波形点绘制到屏幕上。二是波形点缓冲区已满,此时就需要向终端控件一样,将整条波形曲线往左挪一个像素点,空出最右边的一列,最后再将波形点绘制到最后一列即可。因为苹果派使用的是 MCU 屏,显存搬运极其耗时,为了降低 CPU 负担,我们可以选择先用 LCD 的填充函数清空波形控件所在区域,然后再按照波形点缓冲区中记录下来的波形值重新绘制曲线。

输入参数 value 即为波形值,取值范围是 0~widget->height。当然,读者也可以将 value 的取值范围定义为 0-100,这样更好理解和使用。

void GraphAdd(StructGraph* widget, u16 value)
{
  u16 x0, y0, x1, y1, i;
  
  //波形缓冲区未满,直接将波形点添加到波形末端
  if(widget->pointNumCnt < widget->width)
  {
    //第一个点采用画点的方式
    if(0 == widget->pointNumCnt)
    {
      x0 = widget->x;
      y0 = GetYValue(widget, value);
      g_iLCDPointColor = widget->lineColor;
      LCDDrawPoint(x0, y0);
    }
    
    //剩余点采用画线方式
    else
    {
      x0 = widget->x + widget->pointNumCnt - 1;
      y0 = GetYValue(widget, widget->waveBuf[widget->pointNumCnt - 1]);
      x1 = widget->x + widget->pointNumCnt;
      y1 = GetYValue(widget, value);
      g_iLCDPointColor = widget->lineColor;
      LCDDrawLine(x0, y0, x1, y1);
    }
    
    //保存波形点
    widget->waveBuf[widget->pointNumCnt] = value;
    
    //波形点递增
    widget->pointNumCnt++;
  }
  
  //波形缓冲区已满,需要将绘制的点添加到波形点缓冲区末端,然后再刷新显示
  else
  {
    //将波形点添加到缓冲区末端
    for(i = 0; i < (widget->width - 1); i++)
    {
      widget->waveBuf[i] = widget->waveBuf[i + 1];
    }
    widget->waveBuf[widget->width - 1] = value;
    
    //计算波形显示区域
    x0 = widget->x;
    y0 = widget->y;
    x1 = widget->x + widget->width - 1;
    y1 = widget->y + widget->height - 1;
    
    //填充背景
    LCDFill(x0, y0, x1, y1, widget->backColor);
    
    //绘制波形曲线
    for(i = 0; i < (widget->width - 1); i++)
    {
      x0 = widget->x + i;
      y0 = GetYValue(widget, widget->waveBuf[i]);
      x1 = widget->x + i + 1;
      y1 = GetYValue(widget, widget->waveBuf[i + 1]);
      g_iLCDPointColor = widget->lineColor;
      LCDDrawLine(x0, y0, x1, y1);
    }
  }
}

GraphAdd 函数中引用了 GetYValue 函数,GetYValue 函数的定义如下所示,是用来计算当前波形值对应的纵坐标值。对于波形绘制,坐标原点一般位于左下角,而屏幕的原点位于右上角,横坐标方向相同,但纵坐标方向却是反过来了,所以需要做坐标转换。

static u16 GetYValue(StructGraph* widget, u16 value)
{
  i32 y0, y1, yn;
  y0 = widget->y;
  y1 = widget->y + widget->height - 1;
  yn = y1 - value;
  if(yn < y0)
  {
    yn = y0;
  }
  else if(yn > y1)
  {
    yn = y1;
  }
  return yn;
}

验证

波形控件的验证代码如下所示,包含驱动头文件后,首先是要定义波形控件变量,需是静态变量,然后通过 CreateGraph 函数创建波形控件,最后是调用 GraphAdd 向波形控件添加波形显示即可。

因为每添加一个波形点,波形显示区就要整体刷新一遍,所以波形显示会有闪烁的现象,而且也比较卡。

int main(void)
{
  //波形控件
  static StructGraph s_structGraph;
  static unsigned int s_iGraphCnt = 0;
  
  //初始化
  InitHardware();   //初始化硬件相关
  InitSoftware();   //初始化软件相关

  //设置 LCD 初始状态
  LCDDisplayDir(1);
  LCDClear(WHITE);
  
  //创建波形显示控件
  s_structGraph.x = 10;
  s_structGraph.y = 50;
  s_structGraph.width = 780;
  s_structGraph.height = 200;
  s_structGraph.backColor = BLACK;
  s_structGraph.lineColor = GREEN;
  CreateGraph(&s_structGraph);
  
  //清空计数
  s_iGraphCnt = 0;

  while(1)
  {
    //LED 闪烁
    LEDFlicker(100);
    
    //触屏扫描
    //TouchScanTask();
    
    //添加波形点
    GraphAdd(&s_structGraph, s_iGraphCnt);
    s_iGraphCnt = (s_iGraphCnt + 1) % s_structGraph.height;
    
    //延时 10ms
    DelayNms(10);
  }
}

以扫描的方式实现波形绘制

以扫描的方式绘制曲线是指波形每次都从左往右绘制,每新添一个新的波形点时,首先用填充或画线的方式清空将要显示的区域,即绘制缺口,然后再用画线的方式将上一个波形点与当前波形点连接起来,即可完成波形绘制。以扫描方式实现波形绘制的效果如下(没有找到更好的动图,实际没有渐变效果,先顶替一下。。。)

以扫描的方式实现的波形绘制如下所示。在这里可以通过预编译命令设置是使用填充的方式还是画线的方式绘制缺口,使用填充的方式绘制效率会偏低,但显示效果会好一些。

void GraphAdd(StructGraph* widget, u16 value)
{
  u16 x0, y0, x1, y1;
  
#if 0
  //画缺口,缺口的大小为 10
  if((widget->width - widget->pointNumCnt) < 10)
  {
    //确定填充的区域
    x0 = widget->x + widget->pointNumCnt;
    y0 = widget->y;
    x1 = x0 + (widget->width - widget->pointNumCnt) - 1;
    y1 = widget->y + widget->height - 1;
    
    //填充
    LCDFill(x0, y0, x1, y1, widget->backColor);
  }
  else
  {
    //确定填充的区域
    x0 = widget->x + widget->pointNumCnt;
    y0 = widget->y;
    x1 = x0 + 10 - 1;
    y1 = widget->y + widget->height - 1;
    
    //填充
    LCDFill(x0, y0, x1, y1, widget->backColor);
  }
#else
  //画缺口,用画线代替,速度会快一些
  x0 = widget->x + widget->pointNumCnt;
  y0 = widget->y;
  x1 = x0;
  y1 = widget->y + widget->height - 1;
  g_iLCDPointColor = widget->backColor;
  LCDDrawLine(x0, y0, x1, y1);
#endif
    
  //第一列只需要显示一个点
  if(0 == widget->pointNumCnt)
  {
    x0 = widget->x;
    y0 = GetYValue(widget, value);
    g_iLCDPointColor = widget->lineColor;
    LCDDrawPoint(x0, y0);
  }
  
  //不是第一列,需要使用画线的方式将上一个点与当前点连起来
  else
  {
    x0 = widget->x + widget->pointNumCnt - 1;
    y0 = GetYValue(widget, widget->waveBuf[widget->pointNumCnt - 1]);
    x1 = widget->x + widget->pointNumCnt;
    y1 = GetYValue(widget, value);
    g_iLCDPointColor = widget->lineColor;
    LCDDrawLine(x0, y0, x1, y1);
  }

  //保存波形点
  widget->waveBuf[widget->pointNumCnt] = value;

  //更新新的波形点储存位置
  widget->pointNumCnt = (widget->pointNumCnt + 1) % widget->width;
}

总结

在单片机中,因为运算速度有限,所以更多的还是采用扫描的方式绘制波形。如果项目中有要求必须要用滚动的方式绘制波形,为了加快速度,避免闪烁,可以为波形控件开辟一块内存空间,所有的填充、画点、画线操作都在内存中执行,最后再将这部分内存以填充的方式更新到屏幕上,也可以取得比较好的显示效果。

单片机 GUI 设计系列到这里就暂告一段落了,其实读者在不知不觉间已经学会了如何创建按键、如何获取屏幕输入、如何使用图片装饰控件,结合这些知识,相信读者已经能够做一些简单的 GUI 开发,将自己所学的应用到实际的项目中。

后边的窗口管理等内容我想暂时先搁下,主要是代码还没有构思好。后续的章节中可能会先引入文件系统,介绍如何读取 SD 卡中的内容,如何将 SD 卡中的文件拷贝到 Nand Flash 中,还有就是如何通过指令控制 MCU 屏移动显存、如何使用硬件加速在 RGB 屏上绘制位图,等等。

感谢大家的鼎力支持,你们的每一次关注都是我前进的动力。

实验结果

实验结果如下所示。

源码

本章节中的源码请参考《单片机 GUI 设计(零)- 大纲