Bootstrap

【嵌入式】STM32&12864点阵屏使用SimpleGUI单色屏接口库——(1)移植

一 开源库简介

        最近一个项目需要用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 交互引擎HMIhttps://www.bilibili.com/video/av87530421/
06 VirtualSDK概述https://www.bilibili.com/video/av87713369/
07 基于VirtualSDK的GUI开发https://www.bilibili.com/video/BV1qz4y12771/

        最终,通过上面的网址获取到SimpleGUI,其主目录结构和说明如下:

目录名功能
DemoProcSimpGUI的演示代码
Documents关于SimpleGUI的一些简要说明文档
GUISimpleGUI的代码实现部分
HMISimpleGUI的HMI模型实现部分
Images一些图片资源
Transplant/MiniDevCoreSimpleGUI的演示工程
VirtualSDKVirtual 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,编译运行得到如下可交互的演示界面:

这边的移植过程并非重点且比较简单,不再赘述,步骤参考作者提供的说明文档,非常详细:

  1. Documents/01-快速开始SimpleGUI.md (不知何故gitee上直接点击作者工程下的该文件显示“可能包含违规信息”,解决办法是Fork到自己的账号下就可以正常看了)
  2. Documents/A1-搭建基于Codeblocks的wxWidgets开发环境.md

三 移植准备

        作者提供了移植默认演示程序的示例,与上面模拟器中显示的界面是一致的。步骤参考作者提供的说明文档:

  1. Documents/02-移植演示程序.md
  2. 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字库)。定制方法参考资料很多,列举在下面备用:

  1. Documents/How to create font data.md
  2. 【嵌入式】STM32&12864点阵屏使用SimpleGUI单色屏接口库——(2)精简字库-CSDN博客
  3. 关于Simple GUI精简字库的制作方法-CSDN博客
  4. 单色显示屏GUI库simpleGUI使用自定义中文字体资源_freesimplegui-CSDN博客
  5. simplegui自定义字库——UTF8版-CSDN博客
  6. 有关SimpleGUI移植非等宽字符的介绍-CSDN博客

六 实际效果

        至此移植完成, 移植SimpleGUI前后程序编译内存如下所示,大概多了5K多的ROM占用:

        最终的显示效果如图:

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;