基于 GD32F303ZET6 苹果派开发板
简介
主流的 GUI 开发平台,例如 EMWIN,大都有 PC 端的仿真。用户可以像 Qt 一样,在 PC 端完成界面,即 GUI 设计,最终在移植到单片机上。因为仿真可以脱离硬件而运行,所以我们可以随时随地的做开发、学习,只要手里有台电脑就行。本章中我们将介绍如何在 PC 端仿真出单片机下的 GUI。
windows 下 C 语言有个小巧的图形库,叫 EasyX。EasyX 提供了一些基本的 API 接口,例如画点、读点、获取鼠标输入等。虽然 EasyX 很原始,但我们只需要它的画点、读点函数即可。另外吐槽一下,EasyX 的官网在大陆上不去,导致好多资料无从查找,估计得翻墙才行。另外,EasyX 是用 C++ 写的,引用到 EasyX 的文件必须是 C++ 文件,不过还好,C++ 是向下兼容 C 的,所以我们完全可以用 C 的逻辑写代码。
移植
因为 EasyX 官网上不去,所以本文中提供了 EasyX 的安装包,安装包放在文章底部了。另外,推荐使用 Visual Studio 开发平台,EasyX 支持 VC++6.0、Visual Studio2008~2022。
在“相关资料/EasyX 图形库相关/图形库”文件夹下找到 EasyX.exe 文件,双击运行。在安装向导中选择对应的 IDE,即集成开发环境,如下所示。因为我的电脑中装了 Visual Studio 2013 和 Visual Studio 2019,所以我可以选择对这两个 IDE 安装 EasyX。然后,一路 Next 即可。

EasyX 的简单使用
因为 EasyX 使用到了 C++ 的特性,因此只有 C++ 文件才能调用它。EasyX 的头文件是 graphics.h,添加包含 graphics.h 后才能使用 EasyX 的 API 接口。
#include <graphics.h>
EasyX 通过 initgraph 函数创建窗口,具体如下所示。因为是 C++,所以 initgraph 函数支持为参数设定默认值。使能显示终端后,EasyX 会创建两个窗口,一个是应用窗口,另一个是终端窗口,即 C 语言中最常见的黑框。应用窗口用于显示用户界面,终端窗口用于输出一些调试信息,例如 printf 打印输出等。
initgraph(LCD_WIDTH, LCD_HEIGHT); //创建窗口,不显示终端
initgraph(LCD_WIDTH, LCD_HEIGHT, EW_SHOWCONSOLE); //创建窗口,显示终端
EasyX 的画点函数如下所示。经过实际测试,putpixel 函数效率太低,整屏刷很慢,不推荐使用。
putpixel(x, y);
因为 putpixel 函数效率太低,所以在本文提供的源码中,提供了新的思路。我们在 LCD 模块开辟了一段显存空间,也就是定义了一个大数组,打点、读点的对象都是这个显存数组。另外,我们还在 LCD 模块中开辟了一个线程,用于每隔 50ms 将显存数组更新到屏幕显示。具体代码如下所示。因为打点、读点操作的都是我们自己定义的显存缓冲区,所以访问速度会非常快。
本文提供的 LCD 驱动中,通过 LCD.h 中的 LCD_USE_RGB565 宏来配置是否要使用 RGB565格式。因为单片机平台大多使用的像素点格式为 RGB565,而 EasyX 只支持 RGB888,因此要做相应的转换。
static u32 s_arrLCDFrame[LCD_HEIGHT][LCD_WIDTH] = { 0 };
static void LCDReflashThread(void*)
{
u32 x, y, color;
DWORD* image;
u32 i;
while (1)
{
//获取当前显存首地址
image = GetImageBuffer();
//更新到屏幕显示
i = 0;
for (y = 0; y < LCD_HEIGHT; y++)
{
for (x = 0; x < LCD_WIDTH; x++)
{
#if(0 != LCD_USE_RGB565)
color = RGB565ToRGB888B(s_arrLCDFrame[y][x]);
#else
color = s_arrLCDFrame[y][x];
#endif
//更新到显存上
image[i++] = color;
}
}
//延时 50ms
Sleep(50);
}
//线程返回
_endthread();
}
EasyX 获取鼠标输入如下所示。通过 MOUSEMSG 获取到的鼠标信息,坐标系的原点就是应用窗口左上角,而不是电脑屏幕的绝对坐标。我们可以通过 MOUSEMSG 的成员变量 mkLButton 判断鼠标左键是否被按下。如果鼠标在应用窗口范围内按下了左键,那么 mkLButton 将会被置 1。
u32 GetTouch(u32* tx, u32* ty)
{
//获取鼠标信息
MOUSEMSG mouse;
//获取鼠标信息
mouse = GetMouseMsg();
//如果左键被按下
if(1 == mouse.mkLButton)
{
*tx = mouse.x;
*ty = mouse.y;
return 1;
}
else
{
*tx = 0xFFFFFFFF;
*ty = 0xFFFFFFFF;
return 0;
}
}
LCD 底层驱动
为了更贴近于单片机的 GUI 开发,本文模仿裸机下的 GUI 编程,提供了 LCD 底层驱动,源码文件如下所示。LCD 文件对就是 LCD 驱动的顶层文件。为了方便用户使用,我们还将 GBK 字库以常量数组的形式内嵌到了 LCD 驱动中,对应 HzFontxxx.c 文件,也就是说 LCD 驱动是支持中文显示的。注意:因为 GBK 字库很大,所以打开 HzFont64x64.c 文件时,可能会出现卡顿,但不影响编译。

LCD.h 提供的 API 函数如下所示,可以看出,与之前的实验里的 LCD 驱动 API 接口相似的,唯一不同的是,此处将触屏检测也放到了 LCD.c/.h 中,主要是懒得再创建文件对。如果可以的话,我巴不得所有的东西都塞到 LCD 文件对中,这样移植时,只需要添加 LCD 文件对即可。
void InitLCD(void); //初始化 LCD 驱动
void DeInitLCD(void); //注销 LCD 模块
void LCDClear(u32 color); //LCD 清屏
void LCDFill(u32 x0, u32 y0, u32 x1, u32 y1, u32 color); //LCD 填充
void LCDFillColor(u32 x0, u32 y0, u32 x1, u32 y1, u32* color); //LCD 填充
void LCDDrawPoint(u32 x, u32 y); //LCD 画点
void LCDFastDrawPoint(u32 x, u32 y, u32 color); //LCD 快速画点
u32 LCDReadPoint(u32 x, u32 y); //LCD 读点
void LCDDrawLine(u32 x0, u32 y0, u32 x1, u32 y1, u32 color); //LCD 画线
void LCDDrawRectangle(u32 x1, u32 y1, u32 x2, u32 y2, u32 color); //LCD 画矩形
void LCDDrawCircle(u32 x0, u32 y0, u32 r, u32 color); //LCD 画圆
void LCDShowChar(u32 x, u32 y, u32 code, u8 size, u8 mode); //LCD 显示字符
void LCDShowString(u32 x, u32 y, u32 width, u32 height, u8 size, char* p); //LCD 显示字符串
u32 GetTouch(u32* tx, u32* ty); //获取触屏输入,即鼠标输入
LCD 驱动中,屏幕的尺寸由 LCD_WIDTH 和 LCD_HEIGHT 两个宏决定,如下所示。
//LCD 长宽定义
#define LCD_WIDTH (800)
#define LCD_HEIGHT (480)
测试代码
测试代码可以如下所示。
int main(void)
{
char string[64];
u32 x, y;
//初始化 LCD
InitLCD();
//清屏
LCDClear(LCD_COLOR_WHITE);
//字符串显示测试
g_iLCDPointColor = LCD_COLOR_BLACK;
g_iLCDBackColor = LCD_COLOR_WHITE;
LCDShowString(10, 0, LCD_WIDTH, LCD_HEIGHT, 12, (char*)"hello world, 你好世界");
LCDShowString(10, 15, LCD_WIDTH, LCD_HEIGHT, 16, (char*)"hello world, 你好世界");
LCDShowString(10, 30, LCD_WIDTH, LCD_HEIGHT, 24, (char*)"hello world, 你好世界");
LCDShowString(10, 55, LCD_WIDTH, LCD_HEIGHT, 32, (char*)"hello world, 你好世界");
LCDShowString(10, 90, LCD_WIDTH, LCD_HEIGHT, 36, (char*)"hello world, 你好世界");
LCDShowString(10, 125, LCD_WIDTH, LCD_HEIGHT, 64, (char*)"hello world, 你好世界");
//填充测试
LCDFill(10, 200, 790, 230, LCD_COLOR_RED);
//画线测试
LCDDrawLine(10, 240, 790, 470, LCD_COLOR_GREEN);
//画圆测试
LCDDrawCircle(400, 355, 50, LCD_COLOR_BLUE);
//输出鼠标信息
while (1)
{
if (0 != GetTouch(&x, &y))
{
LCDFill(10, 450, 240, 479, g_iLCDBackColor);
sprintf(string, "place: %d %d", x, y);
LCDShowString(10, 450, LCD_WIDTH, LCD_HEIGHT, 12, string);
}
}
return 0;
}
最终的效果如下所示。
-PC-仿真-GUI-实验结果-20230311.bmp)
注意事项
请将项目配置中的“字符集”项目修改为“使用多字节字符集”,否则项目中的字符串编码有可能不是 GBK,造成中文显示乱码。
安装包
源码
本章节中的源码请参考《单片机 GUI 设计(零)- 大纲》