一 开源库简介
最近一个项目需要用12864屏幕呈现一组较为复杂的菜单界面,本着不重复造轮子的原则找到了SimpleGUI开源库。
开源地址:SimpleGUI: 一个面向单色显示屏的开源GUI接口库。
SimpleGUI是一款针对单色显示屏设计的接口库。相比于传统的GUI框架,SimpleGUI的出发点,是在一个单色显示器上,以尽可能少的消耗、尽可能多且直观的表达需要的内容,为此,SimpleGUI抛弃了诸如图层、遮罩、阴影、非等宽字体等高级的、复杂的操作,力求以简单快捷、易于操作的方式,使开发者尽快实现自己需要的功能需求。 同时在满足基本绘图需求的前提下,SimpleGUI还提供了一套被称为HMI的交互引擎,用于统合用户交互、数据处理和屏幕内容绘制与更新,提供了一种简明、易组织、易拓展、低消耗的交互系统。
作者贴心地录制了一些相关操作的视频,粘贴在下面以备不时:
内容 | 地址 |
---|---|
01 SimpleGUI概述 | https://www.bilibili.com/video/av86593220/ |
02 基础绘图 | https://www.bilibili.com/video/av86890300/ |
03 文本文字 | https://www.bilibili.com/video/av87098997/ |
04 拓展组件概述 | https://www.bilibili.com/video/av87432375/ |
05 交互引擎HMI | https://www.bilibili.com/video/av87530421/ |
06 VirtualSDK概述 | https://www.bilibili.com/video/av87713369/ |
07 基于VirtualSDK的GUI开发 | https://www.bilibili.com/video/BV1qz4y12771/ |
最终,通过上面的网址获取到SimpleGUI,其主目录结构和说明如下:
目录名 | 功能 |
---|---|
DemoProc | SimpGUI的演示代码 |
Documents | 关于SimpleGUI的一些简要说明文档 |
GUI | SimpleGUI的代码实现部分 |
HMI | SimpleGUI的HMI模型实现部分 |
Images | 一些图片资源 |
Transplant/MiniDevCore | SimpleGUI的演示工程 |
VirtualSDK | Virtual SDK的工程及源码 |
二 部署模拟器(CodeBlocks)
开源作者提供了一套基于 CodeBlocks & wxWidgets 的模拟开发环境,不依赖于硬件平台即可直接看到显示效果,用来辅助实际界面开发非常有帮助。按需使用,跳过这一步直接着手界面开发亦非不可。
VirtualSDK是SimpleGUI的一个重要组成部分,用于在使用SimpleGUI开发时,脱离硬件平台进行图形界面开发的辅助工具。配合SimpleGUI的低耦合性接移植口定义,使用VirtualSDK开发的用户界面处理源码,几乎额可以无缝的移植到预期的硬件平台上。
步骤分为:
【1】下载CodeBlocks(推荐版本20.03)与wxWidgets(推荐版本3.0.5);
【2】配置CodeBlocks下的wxWidgets开发环境,其中主要关注Settings——Compiler/Debuger/Global variables三项设置:
【3】建立新的wxWidgets Project并配置试用或者直接打开作者提供的工程路径simplegui-Stable\VirtualSDK\Project\CodeBlocks,编译运行得到如下可交互的演示界面:
这边的移植过程并非重点且比较简单,不再赘述,步骤参考作者提供的说明文档,非常详细:
- Documents/01-快速开始SimpleGUI.md (不知何故gitee上直接点击作者工程下的该文件显示“可能包含违规信息”,解决办法是Fork到自己的账号下就可以正常看了)
- Documents/A1-搭建基于Codeblocks的wxWidgets开发环境.md
三 移植准备
作者提供了移植默认演示程序的示例,与上面模拟器中显示的界面是一致的。步骤参考作者提供的说明文档:
- Documents/02-移植演示程序.md
- Documents/How to transplant a platform.md
但是上述移植,一则依赖于额外串口和定时器的实现,二则新增的应用所占内存较大,所以我这边只做最基础的移植,不包含界面交互(通过串口/编码器等外设)和动态显示等,以显示最简单的内容(图形/文字)。移植准备如下:
【1】Keil MDK平台下芯片开发工程以及对12864屏幕简单支持(可以参考我之前的文章:【嵌入式】STM32芯片使用12864串口屏(驱动为ST7567)-CSDN博客);
【2】将SimpleGUI开源库中的GUI、HMI、DemoProc目录复制并添加到当前工程(因为不针对作者提供的DemoProc,所以只用GUI、HMI两个目录中的文件即可);
【3】将GUI、HMI、DemoProc目录下inc文件夹添加到工程的包含路径(Include path)中:
【4】打开GUI\inc目录下的SGUI_Config.h文件,注释掉宏_SIMPLE_GUI_IN_VIRTUAL_SDK_与宏_SIMPLE_GUI_ENABLE_DYNAMIC_MEMORY_:
至此准备工作完成,下面开始软件上面的移植。
四 移植过程
【1】参考开源库中Transplant/MiniDevCore/BSP/src/screen.c文件,结合自身的硬件平台中关于屏幕的实现,需要实现如下几个接口(写点接口/清屏接口/刷新缓存接口,我这边数据类型和具体实现略有改动):
(1)写点接口(LDZ_SetPixel):
#define LCD_SIZE_WIDTH (128)
#define LCD_SIZE_HEIGHT (64)
#define LCD_SIZE_COLUMNS (LCD_SIZE_WIDTH)
#define LCD_SIZE_PAGES (LCD_SIZE_HEIGHT/8)
static struct
{
struct
{
u8 uiStartPageIndex;
u8 uiStartColumnIndex;
u8 uiEndPageIndex;
u8 uiEndColumnIndex;
}stUpdateArea;
u8 auiDisplayCache[LCD_SIZE_WIDTH][LCD_SIZE_PAGES];
}s_stLCDBuffer;
static void LDZ_UpdateChangedBufferAreaRecord(u8 uiPageIndex, u8 uiColumnIndex);
static void LDZ_ClearBuf(void);
/*************************************************************************/
/** Function Name: LDZ_UpdateChangedBufferAreaRecord **/
/** Purpose: Check changed area recodr and update. **/
/** Resources: None. **/
/** Params: **/
/** @ uiPageIndex: Operated page index. **/
/** @ uiColumnIndex: Operated column index. **/
/** Return: None. **/
/** Limitation: None. **/
/*************************************************************************/
void LDZ_UpdateChangedBufferAreaRecord(u8 uiPageIndex, u8 uiColumnIndex)
{
// Check and set page and column index.
if(uiPageIndex < s_stLCDBuffer.stUpdateArea.uiStartPageIndex)
{
s_stLCDBuffer.stUpdateArea.uiStartPageIndex = uiPageIndex;
}
if(uiPageIndex > s_stLCDBuffer.stUpdateArea.uiEndPageIndex)
{
s_stLCDBuffer.stUpdateArea.uiEndPageIndex = uiPageIndex;
}
if(uiColumnIndex < s_stLCDBuffer.stUpdateArea.uiStartColumnIndex)
{
s_stLCDBuffer.stUpdateArea.uiStartColumnIndex = uiColumnIndex;
}
if(uiColumnIndex > s_stLCDBuffer.stUpdateArea.uiEndColumnIndex)
{
s_stLCDBuffer.stUpdateArea.uiEndColumnIndex = uiColumnIndex;
}
}
/*************************************************************************/
/** Function Name: LDZ_SetPixel **/
/** Purpose: Set a pixel color or draw a point. **/
/** Resources: None. **/
/** Params: **/
/** @ iPosX: X location of point by pixels. **/
/** @ iPosY: Y location of point by pixels. **/
/** @ iColor: Point color, 0 is white, Nonzero is Black. **/
/** Return: None. **/
/** Limitation: None. **/
/*************************************************************************/
void LDZ_SetPixel(int iPosX, int iPosY, unsigned int iColor)
{
// Operating position check.
if((iPosX < LCD_SIZE_WIDTH) && (iPosY < LCD_SIZE_HEIGHT))
{
// Check and set page and column index.
LDZ_UpdateChangedBufferAreaRecord(iPosY/8, iPosX);
// Set point data.
if(1 == iColor)
s_stLCDBuffer.auiDisplayCache[iPosX][iPosY/8] |= (1 << iPosY%8);
else
s_stLCDBuffer.auiDisplayCache[iPosX][iPosY/8] &= ~(1 << iPosY%8);
}
}
(2)清屏接口(LDZ_Clear):
/*************************************************************************/
/** Function Name: LDZ_ClearBuf **/
/** Purpose: Clean display buffer. **/
/** Resources: None. **/
/** Params: None. **/
/** Return: None. **/
/** Limitation: None. **/
/*************************************************************************/
void LDZ_ClearBuf(void)
{
u8 uiCurrentPageIndex, uiCurrentColumnIndex;
for(uiCurrentPageIndex = 0; uiCurrentPageIndex < LCD_SIZE_PAGES; uiCurrentPageIndex++)
{
for(uiCurrentColumnIndex = 0; uiCurrentColumnIndex < LCD_SIZE_WIDTH; uiCurrentColumnIndex++)
{
s_stLCDBuffer.auiDisplayCache[uiCurrentColumnIndex][uiCurrentPageIndex] = 0x00;
}
}
s_stLCDBuffer.stUpdateArea.uiStartPageIndex = (LCD_SIZE_HEIGHT/8) - 1;
s_stLCDBuffer.stUpdateArea.uiEndPageIndex = 0;
s_stLCDBuffer.stUpdateArea.uiStartColumnIndex = LCD_SIZE_WIDTH - 1;
s_stLCDBuffer.stUpdateArea.uiEndColumnIndex = 0;
}
/*************************************************************************/
/** Function Name: LDZ_Clear **/
/** Purpose: Clean display buffer. **/
/** Resources: None. **/
/** Params: None. **/
/** Return: None. **/
/** Limitation: None. **/
/*************************************************************************/
void LDZ_Clear(void)
{
u8 i,j;
LDZ_ClearBuf();
for(i = 0;i < 8;i ++) //一共8行
{
LDZ_SetAddr(i, 0);
for(j = 0;j < 128;j ++) //一行128字节
LDZ_WriteDAT(0x00);
}
}
(3)刷新缓存接口(LDZ_Refresh):
/*************************************************************************/
/** Function Name: LDZ_Refresh **/
/** Purpose: Update Screen with cache data. **/
/** Resources: None. **/
/** Params: None. **/
/** Return: None. **/
/** Limitation: None. **/
/*************************************************************************/
void LDZ_Refresh(void)
{
u8 uiChangedPageIndex, uiChangedColumnIndex;
if(s_stLCDBuffer.stUpdateArea.uiEndColumnIndex > LCD_SIZE_WIDTH-1)
{
s_stLCDBuffer.stUpdateArea.uiEndColumnIndex = LCD_SIZE_WIDTH-1;
}
if(s_stLCDBuffer.stUpdateArea.uiEndPageIndex > (LCD_SIZE_HEIGHT/8)-1)
{
s_stLCDBuffer.stUpdateArea.uiEndPageIndex = (LCD_SIZE_HEIGHT/8)-1;
}
uiChangedPageIndex = s_stLCDBuffer.stUpdateArea.uiStartPageIndex;
// Loop for each changed page.
while(uiChangedPageIndex <= s_stLCDBuffer.stUpdateArea.uiEndPageIndex)
{
uiChangedColumnIndex = s_stLCDBuffer.stUpdateArea.uiStartColumnIndex;
LDZ_SetAddr(uiChangedPageIndex, s_stLCDBuffer.stUpdateArea.uiStartColumnIndex);
// Loop for each changed column data in current page.
while(uiChangedColumnIndex <= s_stLCDBuffer.stUpdateArea.uiEndColumnIndex)
{
// Write data to screen controler.
LDZ_WriteDAT(s_stLCDBuffer.auiDisplayCache[uiChangedColumnIndex][uiChangedPageIndex]);
uiChangedColumnIndex++;
}
uiChangedPageIndex++;
}
// Reset global variable.
s_stLCDBuffer.stUpdateArea.uiStartPageIndex = (LCD_SIZE_HEIGHT/8) - 1;
s_stLCDBuffer.stUpdateArea.uiEndPageIndex = 0;
s_stLCDBuffer.stUpdateArea.uiStartColumnIndex = LCD_SIZE_WIDTH - 1;
s_stLCDBuffer.stUpdateArea.uiEndColumnIndex = 0;
}
(4)在主函数main中向设备对象模型注册屏幕设备(我这边用了操作系统,所以是在子线程中实现):
/**************************************************************************
* 函数名称: Gui_Task
* 功能描述: 液晶显示子任务
**************************************************************************/
SGUI_SCR_DEV disp_dev; //中文屏设备源
#define BMP_DATA_BUFFER_SIZE (512)
SGUI_BYTE s_pBmpDataBuffer[BMP_DATA_BUFFER_SIZE];
void Gui_Task(void)
{
SGUI_RECT disp_area;
SGUI_POINT disp_offs;
//点阵屏初始化/配置命令/清屏
LDZ_Init();
LDZ_Cfg();
LDZ_Clear();
//设备内存地址初始化
SGUI_SystemIF_MemorySet(&disp_dev, 0x00, sizeof(disp_dev));
//一些相关初始化配置
disp_area.iX = 0;
disp_area.iY = 0;
disp_area.iWidth = 128;
disp_area.iHeight = 64;
disp_offs.iX = 0;
disp_offs.iY = 0;
disp_dev.stSize.iWidth = 128;
disp_dev.stSize.iHeight = 64;
/* Initialize interface object. */
disp_dev.fnSetPixel = LDZ_SetPixel;
disp_dev.fnClear = LDZ_Clear;
disp_dev.fnSyncBuffer = LDZ_Refresh;
disp_dev.stBuffer.pBuffer = s_pBmpDataBuffer;
disp_dev.stBuffer.sSize = BMP_DATA_BUFFER_SIZE;
//...
while(1)
{
//例1:显示矩形
SGUI_Basic_DrawRectangle(&disp_dev, 10, 10, 40, 20, 1, 0);
//例2:显示字符
disp_area.iX = 64;
disp_area.iY = 16;
disp_area.iWidth = 128;
disp_area.iHeight = 16;
SGUI_Text_DrawText(&disp_dev, "0123", &SGUI_DEFAULT_FONT_8, &disp_area, &disp_offs, SGUI_DRAW_NORMAL);
//例3:显示中文字符
disp_area.iX = 64;
disp_area.iY = 32;
SGUI_Text_DrawText(&disp_dev, "中国万岁", &SGUI_DEFAULT_FONT_16, &disp_area, &disp_offs, SGUI_DRAW_NORMAL); //显示中文需要UTF8编码
LDZ_Refresh();
os_dly_wait(50);
}
}
其中,向设备对象注册写点接口/清屏接口/刷新缓存接口是下面几句:
/* Initialize interface object. */
disp_dev.fnSetPixel = LDZ_SetPixel;
disp_dev.fnClear = LDZ_Clear;
disp_dev.fnSyncBuffer = LDZ_Refresh;
至此基础的移植完成,下面是使用字库的步骤。
五 字库调用
开源库中的字库位置在 GUI\src\SGUI_FontResource.c 文件中。出于简单方便的原则,用自带的字库 SGUI_DEFAULT_FONT_8/SGUI_DEFAULT_FONT_16 进行测试。
其中需要注意以下几点:
【1】我在调用字库的过程中出现了显示形状没问题,但是显示字符乱码的情况。
经调试发现,初始化中需要加上stBuffer.pBuffer / stBuffer.sSize两行定义(定义一个512字节的数组),源码中字符绘制依赖这个该数据区的判断,如果为空会判断失败。如下图所示:
【2】调用SGUI_DEFAULT_FONT_16字库显示中文的过程中,出现显示中文乱码的问题:
问题原因,为了能使用默认字库,包含中文的.c文件需UTF-8编码格式,而我的源文件是GB格式,所以会出现乱码。解决办法是用文本处理软件(我用的是Notepad--)转换一下源文件的格式即可:
【3】实际使用中,默认的字库内容肯定不够,需要自定义字库(基于Flash空间的考虑,推荐使用精简的定制字库 / 基于使用方便的考虑,推荐使用整个GB2312字库)。定制方法参考资料很多,列举在下面备用:
- Documents/How to create font data.md
- 【嵌入式】STM32&12864点阵屏使用SimpleGUI单色屏接口库——(2)精简字库-CSDN博客
- 关于Simple GUI精简字库的制作方法-CSDN博客
- 单色显示屏GUI库simpleGUI使用自定义中文字体资源_freesimplegui-CSDN博客
- simplegui自定义字库——UTF8版-CSDN博客
- 有关SimpleGUI移植非等宽字符的介绍-CSDN博客
六 实际效果
至此移植完成, 移植SimpleGUI前后程序编译内存如下所示,大概多了5K多的ROM占用:
最终的显示效果如图: