基于 GD32F303ZET6 苹果派开发板
简介
仪表盘,广泛应用于汽车显示速度,也可以用来做时钟面板、温湿度计面板等等,应用十分广泛,在生活中随处可见。汽车的仪表盘如下所示。(本想做一段生动的描述,奈何文化水平不够)

仪表盘驱动难点在于指针的绘制,好多同学不知道如何让指针转起来。想要显示指针并不难,难就难在如何让指针消失,总不至于画着画着,屏幕上满是指针吧,要遵循只有一个指针原则。本文中将介绍四种可以让指针转起来的办法。
方法一:暴力刷图
刷图是实现仪表盘最简单、最暴力的方法。如果你的仪表盘有一百个分度格,那么就直接准备一百张指针角度不通的图片,需要显示哪个角度时,直接显示对应的图片即可。简单、实用,就是不怎么方便,因为要准备好多张图片,想想就心累。
方法二:刷整屏
首先要找到一张干净的仪表盘图片,必须是不带指针的那种,如下所示。当然,不一定非得要整张图片,读者也可以用画弧线的方式,手绘出一个不带指针的仪表盘图案。就我个人而言,更倾向于使用图片,因为简单方便,刷图就完事了。

使用刷整屏的方式实现指针转动原理很简单,就是每次绘制指针之前,用一张带仪表盘的图片刷新整个屏幕,然后再将指针描上去。刷屏的过程中会很自然的将上一个指针抹去,重新绘制指针后,仪表盘上就只剩下一条指针了。
这个方法适用于 GD32F470、STM32F429 等带硬件刷图机制的处理器。这些处理器刷图的速度很快,而且通常会带有很大的外拓 SDRAM,一般是 32M,比单片机内部 Flash 还要大得多。使用时可以提前将整张背景图片从文件系统导入到内存,即 SDRAM 中,刷图时直接用硬件加速机制,将整张图片填充到屏幕显示。硬件刷图通常很快,肉眼几乎看不到刷图过程。
对于使用了 MCU 屏的苹果来,因为 GD32F303 不支持硬件刷图,而且外拓的 SRAM 只有 1M,所有拿图片刷整屏会比较慢。一般都是用纯色刷整屏,然后再用画弧线函数,描绘出仪表盘,这样速度会快一些。当然,读者也可以将图片显示范围缩小,刷整屏时用纯色,然后再将仪表盘图片显示到对应位置即可,这样也可以得到一个不错的刷新速度,就是仪表盘图片别太大,会很慢。。。。
对于指针,前期我们可以简单的用一条直线代替,如此一来,绘制指针时,只需要调用画线函数即可。
使用刷整屏实现指针转动的代码如下所示。指针的起点,也就是仪表盘中心的位置比较难确定,一般我是通过打点函数,尝试多次后才能得到理想位置。知道了起点,确定了指针的长度之后,需要用量角器测量速度为 0 r/min 和 1200 r/min 两个转速对应的角度,然后将传进来的速度值转换成百分比,紧接着就可以用三角函数计算指针的终点了。得到了指针的起点和终点之后,直接用画线函数将两个坐标点连起来即可。
#include "./LCD/LCD.h"
#include "./Picture/BMP.h"
#include <math.h>
//指针相关信息
#define POINTER_X0 (400)
#define POINTER_Y0 (260)
#define POINTER_LEN (200)
#define POINTER_COLOR (LCD_COLOR_RED)
//绘制背景
static void DrawBackground(void)
{
extern const unsigned char g_arrSpeedBgImage800x480[768068];
DrawBMP(g_arrSpeedBgImage800x480, 0, 0);
}
//设置速度仪表盘速度显示
//speed:0~1200
void SetSpeedometer(unsigned int speed)
{
double pi, angle0, angle1, angle, rate;
int x, y, x0, y0, x1, y1;
//限定范围
if (speed > 1200)
{
speed = 1200;
}
//锁定 LCD
LCDLock();
//绘制背景
DrawBackground();
//计算占比大小
rate = 100.0 * (double)speed / 1200.0;
rate = 100.0 - rate;
//求直线在直角坐标系中的角度
pi = 3.1415926535;
angle0 = pi * -0.2445; //100%对应的角度(弧度)
angle1 = pi * 1.2445; //0%对应的角度(弧度)
angle = angle0 + (angle1 - angle0) * rate / 100.0;
//求直线起点终点
x = POINTER_LEN * cos(angle);
y = POINTER_LEN * sin(angle);
y = -y;
x0 = POINTER_X0;
y0 = POINTER_Y0;
x1 = x0 + x;
y1 = y0 + y;
//绘制直线
LCDDrawLine(x0, y0, x1, y1, POINTER_COLOR);
//解锁 LCD
LCDUnlock();
}
方法三:用纯色刷新指针所在区域
刷图固然简单,但是卡慢,苹果派开发板承受不来。光是仪表盘显示,可能就耗光了苹果派的算力。如果仪表盘的元素比较简单,背景是纯色的,就像本文提供的那种图片一样。这时候我们可以选择将指针长度缩短一些,用纯色刷新指针显示区域。
这个方法具体步骤是,首先规划指针显示的矩形区域,注意,必须是矩形,且矩形区域不能覆盖到仪表盘的刻度尺上,然后每次显示指针之前,用纯色(与仪表盘背景颜色相同)填充这片矩形区域,最后将指针画上去即可。因为指针显示的区域有限制,所以用此方法时,指针一般不会太长,且不会延伸到仪表盘的刻度尺上。
具体实现代码如下所示。同样的,刷新区域要通过 LCDDrawRectangle 函数多次标定,才会得到比较理想的值。因为是纯色填充,会显得指针起点比较孤单,为了仪式感,我们可以以指针起点为圆心,绘制一个空心圆,或实心圆。
#include "./LCD/LCD.h"
#include "./Picture/BMP.h"
#include <math.h>
//刷新区域
#define POINTER_AREA_X0 (300)
#define POINTER_AREA_Y0 (160)
#define POINTER_AREA_X1 (500)
#define POINTER_AREA_Y1 (360)
#define POINTER_AREA_WIDTH (POINTER_AREA_X1 - POINTER_AREA_X0 + 1)
#define POINTER_AREA_HEIGHT (POINTER_AREA_Y1 - POINTER_AREA_Y0 + 1)
//指针相关信息
#define POINTER_X0 ((POINTER_AREA_X1 + POINTER_AREA_X0) / 2)
#define POINTER_Y0 ((POINTER_AREA_Y1 + POINTER_AREA_Y0) / 2)
#define POINTER_LEN (POINTER_AREA_WIDTH / 2)
#define POINTER_COLOR (LCD_COLOR_RED)
//背景颜色,初始化时自动读取
static unsigned int s_iBackColor;
//绘制背景
static void DrawBackground(void)
{
extern const unsigned char g_arrSpeedBgImage800x480[768068];
DrawBMP(g_arrSpeedBgImage800x480, 0, 0);
}
//初始化速度仪表盘
void InitSpeedometer(void)
{
//绘制背景图片
DrawBackground();
//读取背景色
s_iBackColor = LCDReadPoint(POINTER_X0, POINTER_Y0);
//标记矩形区域
//LCDDrawRectangle(POINTER_AREA_X0, POINTER_AREA_Y0, POINTER_AREA_X1, POINTER_AREA_Y1, POINTER_COLOR);
//初始化时速度显示值为 0 r/min
SetSpeedometer(0);
}
//设置速度仪表盘速度显示
//speed:0~1200
void SetSpeedometer(unsigned int speed)
{
double pi, angle0, angle1, angle, rate;
int x, y, x0, y0, x1, y1;
//限定范围
if (speed > 1200)
{
speed = 1200;
}
//锁定 LCD
LCDLock();
//刷新指针区域背景
LCDFill(POINTER_AREA_X0, POINTER_AREA_Y0, POINTER_AREA_X1, POINTER_AREA_Y1, s_iBackColor);
//计算占比大小
rate = 100.0 * (double)speed / 1200.0;
rate = 100.0 - rate;
//求直线在直角坐标系中的角度
pi = 3.1415926535;
angle0 = pi * -0.2445; //100%对应的角度(弧度)
angle1 = pi * 1.2445; //0%对应的角度(弧度)
angle = angle0 + (angle1 - angle0) * rate / 100.0;
//求直线起点终点
x = POINTER_LEN * cos(angle);
y = POINTER_LEN * sin(angle);
y = -y;
x0 = POINTER_X0;
y0 = POINTER_Y0;
x1 = x0 + x;
y1 = y0 + y;
//绘制直线
LCDDrawLine(x0, y0, x1, y1, POINTER_COLOR);
//画圆,突出圆心位置(有些半径会显得不够圆,应该是驱动问题)
LCDDrawCircle(x0, y0, 21, POINTER_COLOR);
//解锁 LCD
LCDUnlock();
}
方法四:使用填回背景的方式刷新指针显示区域
如果仪表盘的背景不是纯色的,带了一些图案或者是渐变色,那么就不能简单的用纯色刷了。此时我们依旧可以事先规划指针显示区域,然后在初始化时,将这部分区域的像素点值读取到内存中,每次绘制指针前,先填回背景,然后在画指针,这样也能实现指针的转动,具体如下所示。
LCD 显示时,纯色填充和使用缓冲区填充速度上差不多,所以方法三同样适用于苹果派。但是,方法三也有局限性,那就是指针显示区域不能太大,因为背景缓冲区会占据大量内存,很容易造成内存不足的困境。不过,对于 GD32F470 或 STM32F429 等平台,这都不是事,刷就完事了。
#include "./LCD/LCD.h"
#include "./Picture/BMP.h"
#include <math.h>
//刷新区域
#define POINTER_AREA_X0 (300)
#define POINTER_AREA_Y0 (160)
#define POINTER_AREA_X1 (500)
#define POINTER_AREA_Y1 (360)
#define POINTER_AREA_WIDTH (POINTER_AREA_X1 - POINTER_AREA_X0 + 1)
#define POINTER_AREA_HEIGHT (POINTER_AREA_Y1 - POINTER_AREA_Y0 + 1)
//指针相关信息
#define POINTER_X0 ((POINTER_AREA_X1 + POINTER_AREA_X0) / 2)
#define POINTER_Y0 ((POINTER_AREA_Y1 + POINTER_AREA_Y0) / 2)
#define POINTER_LEN (POINTER_AREA_WIDTH / 2)
#define POINTER_COLOR (LCD_COLOR_RED)
//背景缓冲区
#if(0 != LCD_USE_RGB565)
static unsigned short s_arrBackground[POINTER_AREA_WIDTH * POINTER_AREA_HEIGHT]; //RGB565
#else
static unsigned int s_arrBackground[POINTER_AREA_WIDTH * POINTER_AREA_HEIGHT]; //RGB888
#endif
//绘制背景
static void DrawBackground(void)
{
extern const unsigned char g_arrSpeedBgImage800x480[768068];
DrawBMP(g_arrSpeedBgImage800x480, 0, 0);
}
//初始化速度仪表盘
void InitSpeedometer(void)
{
unsigned int x, y, i;
//绘制背景图片
DrawBackground();
//保存指针区域背景
i = 0;
for (y = POINTER_AREA_Y0; y <= POINTER_AREA_Y1; y++)
{
for (x = POINTER_AREA_X0; x <= POINTER_AREA_X1; x++)
{
s_arrBackground[i++] = LCDReadPoint(x, y);
}
}
//标记矩形区域
//LCDDrawRectangle(POINTER_AREA_X0, POINTER_AREA_Y0, POINTER_AREA_X1, POINTER_AREA_Y1, POINTER_COLOR);
//初始化时速度显示值为 0 r/min
SetSpeedometer(0);
}
//设置速度仪表盘速度显示
//speed:0~1200
void SetSpeedometer(unsigned int speed)
{
double pi, angle0, angle1, angle, rate;
int x, y, x0, y0, x1, y1, i;
//限定范围
if (speed > 1200)
{
speed = 1200;
}
//锁定 LCD
LCDLock();
//重绘指针区域背景
i = 0;
for (y = POINTER_AREA_Y0; y <= POINTER_AREA_Y1; y++)
{
for (x = POINTER_AREA_X0; x <= POINTER_AREA_X1; x++)
{
LCDFastDrawPoint(x, y, s_arrBackground[i++]);
}
}
//计算占比大小
rate = 100.0 * (double)speed / 1200.0;
rate = 100.0 - rate;
//求直线在直角坐标系中的角度
pi = 3.1415926535;
angle0 = pi * -0.2445; //100%对应的角度(弧度)
angle1 = pi * 1.2445; //0%对应的角度(弧度)
angle = angle0 + (angle1 - angle0) * rate / 100.0;
//求直线起点终点
x = POINTER_LEN * cos(angle);
y = POINTER_LEN * sin(angle);
y = -y;
x0 = POINTER_X0;
y0 = POINTER_Y0;
x1 = x0 + x;
y1 = y0 + y;
//绘制直线
LCDDrawLine(x0, y0, x1, y1, POINTER_COLOR);
//解锁 LCD
LCDUnlock();
}
实验结果
实验结果如下所示。用直线代替指针实在是太单调了,下一章中将介绍如何通过多边形填充的方式,绘制菱形指针。
-仪表盘-实验结果-20230313.bmp)
源码
本章节中的源码请参考《单片机 GUI 设计(零)- 大纲》