emWin 是由德国 SEGGER 公司开发,可为图形 LCD 设计提供高级支持,极大简化了 LCD 设计。 为恩智浦ARM 微控制器用户免费提供的 emWin 图形库。
在国内做嵌入式系统的大部分都使用 emwin, 其简单来说就是一套图形库。
STemWin是SEGGER公司授权给ST(意法半导体)的。使用ST芯片的用户可以免费使用STemWin。其实不光授权给了ST,还有NXP,Energy Micro等。凡是使用这些芯片厂商生产的处理器都可以免费的使用emWin。但是出于一定的保护措施,使用STemWin的库是不能用在其它芯片厂商的处理器上面的。因为在工程初始化STemWin前要使能CRC校验。如果没有使能,STemWin是启动不起来的。
STemWin还针对ST的微控制器做了专门的优化,比如在使用ST的F4XX微控制器带FPU的芯片时,STemWin在需要浮点处理的地方专门做了优化。
本文详细介绍了在STM32F1xx开发板上移植STemWin的步骤。
(1)上
st.com
官网下载STemWin的压缩包:
打开浏览器,输入地址
https://www.st.com/zh/embedded-software/stemwin.html
,打开STemWin官网界面如下:

点击界面上的“获取软件”链接,进入软件下载部分:

点击“Get latest”红色按钮,弹出许可协议按钮:

点击“接受”蓝色按钮,如果登录了,则直接开始下载,如果没有登录,则需要登录
my.st.com
账号,没有账号,注册一个账号再登录即可下载。
下载得到“en.stemwin.zip”压缩文件,解压后得到“STemWin_Library_V1.2.0”文件夹,包含以下4个文件夹:

-
_htmresc
//ST的logo图片;
-
Libraries
//各种源码库文件,详见下文;
-
Project
//各个官方开发板下的程序例程;
-
Utilites
//一些工具代码;
-
Release_Notes.html
//版本说明连接。
跟移植STemWin有关的是Libraries文件夹。Libraries文件夹打开后如下:

-
CMSIS
//存放符合CMSIS标准的文件,包括STM32启动文件、ARM Cortex内核文件和对应外设头文件stm32fxxx.h;
-
STemWinLibrary532
//EmWin的库文件;
-
STM32FXXX_StdPeriph_Driver
//存放STM32外设驱动文件,inc目录用于存放外设的头文件,src目录用于存放外设的源文件。

跟移植STemWin有关的是4个文件夹:Config、inc、Lib、和OS。另外Documentation文件夹中有STemWin的参考手册,Simulation文件夹中有关于PC端仿真用到的vs工程文件,Software中有一些工具软件。
(2)拷贝相关的STemWin库文件和文件夹到uVision5的项目目录
要将STemWin库移植到我们自己的STM32程序中,需要将Config、inc、Lib、和OS这4个文件夹拷贝到我们自己的项目文件夹下,例如,在项目根目录下建立一个STemWin目录,然后,将这4个文件夹拷贝进去:

在Lib文件夹中,包含了所有STM32芯片的ARM核的库文件,库文件的命名方式是“STemWin版本_ARM核_是否带OS_开发编译环境.a/lib”。

需要根据自己项目的芯片的ARM核选择不同的,不属于自己项目的芯片的ARM核的库文件可以直接删除。例如,我下面的例子是在STM32F103(CM3核)芯片的板子上,用uVision5(Keil),不带OS的环境下,将STemWin移植到我自己的项目中,所以,我只保留了STemWin532_CM3_Keil.lib文件,其它文件都删除了:

至此,移植需要的STemWin的相关文件都已经拷贝到项目所在目录下,下一步是将需要添加到项目和需要修改的文件添加进项目。
我们可以在项目下建立一个Group,例如,我建立的Group是STemWin,然后,在STemWin中添加库中的6个文件如下:
Config\GUIConf.h
Config\GUIConf.c
Config\GUIDRV_Template.c
Config\LCDConf_FlexColor_Template.c
OS\GUI_X.c
//GUI_X.c用于没有使用OS的程序的移植,如果使用了OS,需要改成OS\GUI_X_OS.c
Lib\STemWin532_CM3_Keil.lib
另外,还需要建立2个文件:
一个是空内容的文件LCDConf.h,这个文件放置在代码可以引用的位置,STemWin库会引用该文件,因此,必须创建一个该文件,其内容可以是空;
另一个文件是GUI_X_Touch_Analog.c,这个文件用于STemWin实现触摸屏操控。
至此,创建的Group结构如下:

到这里,要移植的库文件已经全了,后面会说明如何修改这些文件。
(3)测试验证显示屏和触摸屏的驱动函数是否有效
要在STM32嵌入式系统上显示STemWin的界面,除了需要STemWin的库文件,还需要有显示屏的驱动文件和触摸屏的驱动文件。
STM32的显示屏的驱动有2种方式,一种是通过读写寄存器的方式实现LCD显示屏的显示,另一种是通过STM32的DMA2D图形加速功能的LTDC驱动程序实现LCD显示屏的显示。
移植STemWin时,要搞清楚板子的LCD屏使用的是那种驱动方式。不管LCD使用哪种驱动方式,都最好使用统一的接口,方便以后的代码复用。
STemWin种LCD的显示,主要调用2个函数,一个是LCD显示某个点的颜色的函数,一个是读取LCD某个点的颜色值的函数,另外,针对填充色块(FillRect)和显示位图(DrawBitLineXBPP)不同显示屏可能有些不同的优化函数,移植时,可以让STemWin调用这些优化函数。测试显示屏的显示某个点的颜色和读取某个点的颜色的驱动函数是否有效,以及填充色块和显示位图的驱动优化函数(如果有的话)是否有效。
STemWin对触摸屏的驱动函数的调用,只有4个:GUI_TOUCH_X_ActivateX、GUI_TOUCH_X_ActivateY、GUI_TOUCH_X_MeasureX、GUI_TOUCH_X_MeasureY,前2个函数是用于使能触摸屏的轴线,后面2个函数是用于获取触摸点在触摸屏上的轴坐标。测试触摸屏的驱动函数是否可以获取到触摸点在触摸屏上的横纵坐标。
显示屏的显示驱动函数和触摸驱动函数验证通过后,就可以修改拷贝过来的STemWin的库文件了。
(4)修改拷贝过来的STemWin的库文件
(4.1)GUIConf.h文件修改:
//这个文件主要配置一些功能开关,如下:
#define GUI_NUM_LAYERS 1 //UI层数,每层都可设置指定API
#define GUI_OS (0) //是否有OS
#define GUI_SUPPORT_TOUCH (1) //是否带触摸屏
#define GUI_DEFAULT_FONT &GUI_Font6x8
#define GUI_SUPPORT_MOUSE (1) /* Support a mouse */
#define GUI_WINSUPPORT (1) /* Use window manager */
#define GUI_SUPPORT_MEMDEV (1) /* Memory device package available */
#define GUI_SUPPORT_DEVICES (1) /* Enable use of device pointers */
(4.2)GUIConf.c文件修改:
//本文件定义了GUI_X_Config函数,该函数会被STemWin的GUI_Init()函数调用,我们需要在该函数内为STemWin分配内存:
#define GUI_NUMBYTES (20 * 1024) //20k内存很小,一般建议使用2M内存以上
//#define GUI_NUMBYTES (2 * 1024 * 1024) __attribute__((at(0XC0000000))); //如果要在外部SDRAM上申请内存,可以通过at指定内存起始位置
#define GUI_BLOCKSIZE 0X80
U32 aMemory[GUI_NUMBYTES / 4];
void GUI_X_Config(void) {
U32* aMemory2 = &aMemory[0];
GUI_ALLOC_AssignMemory((U32*)aMemory2, GUI_NUMBYTES);
GUI_ALLOC_SetAvBlockSize(GUI_BLOCKSIZE);
GUI_SetDefaultFont(GUI_FONT_6X8);
}
(4.3)GUIDRV_Template.c文件修改:
//该文件内定义了STemWin调用显示驱动函数在显示屏上显示的函数,必须修改的函数有2个(_SetPixelIndex和_GetPixelIndex),其他FillRect和DrawBitLineXBPP函数按驱动函数规范修改:
static void _SetPixelIndex(GUI_DEVICE * pDevice, int x, int y, int PixelIndex) {
LCD_DrawPoint(x, y, PixelIndex);
};
static unsigned int _GetPixelIndex(GUI_DEVICE * pDevice, int x, int y) {
unsigned int PixelIndex;
PixelIndex = LCD_ReadPoint(x,y);
return PixelIndex;
}
(4.4)LCDConf_FlexColor_Template.c文件修改:
//该文件内定义了显示屏的尺寸,以及另外一个要修改的函数时LCD_X_Config(),它会被STemWin的GUI_Init()函数调用
#define XSIZE_PHYS 320 // To be adapted to x-screen size
#define YSIZE_PHYS 240 // To be adapted to y-screen size
#ifndef VXSIZE_PHYS
#define VXSIZE_PHYS XSIZE_PHYS
#endif
#ifndef VYSIZE_PHYS
#define VYSIZE_PHYS YSIZE_PHYS
#endif
void LCD_X_Config(void) {
int i;
#if (NUM_BUFFERS > 1)
for (i = 0; i < GUI_NUM_LAYERS; i++)
{
GUI_MULTIBUF_ConfigEx(i, NUM_BUFFERS);
}
#endif
//注意,该参数必须是&GUIDRV_Template_API和GUICC_M565(不是GUICC_565)
GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0);
//设置显示屏尺寸
LCD_SetSizeEx(0,lcddev.width,lcddev.height);
LCD_SetVSizeEx(0,lcddev.width,lcddev.height);
//设置触摸屏横纵轴取值范围
GUI_TOUCH_Calibrate(GUI_COORD_X,0,lcddev.width,0,lcddev.width-1);
GUI_TOUCH_Calibrate(GUI_COORD_Y,0,lcddev.height,0,lcddev.height-1);
//
// Setting up VRam address and custom functions for CopyBuffer-, CopyRect- and FillRect operations
//
for (i = 0; i < GUI_NUM_LAYERS; i++)
{
_aPendingBuffer[i] = -1;
//
// Set VRAM address
//
LCD_SetVRAMAddrEx(i, (void *)(_aAddr[i]));
//
// Remember color depth for further operations
//
_aBytesPerPixels[i] = LCD_GetBitsPerPixelEx(i) >> 3;
//
// Set custom functions for several operations
//
LCD_SetDevFunc(i, LCD_DEVFUNC_COPYBUFFER, (void(*)(void))_LCD_CopyBuffer);
LCD_SetDevFunc(i, LCD_DEVFUNC_COPYRECT, (void(*)(void))_LCD_CopyRect);
//
// Filling via DMA2D does only work with 16bpp or more
//
if (_GetPixelformat(i) <= LTDC_Pixelformat_ARGB4444)
{
LCD_SetDevFunc(i, LCD_DEVFUNC_FILLRECT, (void(*)(void))_LCD_FillRect);
LCD_SetDevFunc(i, LCD_DEVFUNC_DRAWBMP_8BPP, (void(*)(void))_LCD_DrawBitmap8bpp);
}
//
// Set up drawing routine for 16bpp bitmap using DMA2D
//
if (_GetPixelformat(i) == LTDC_Pixelformat_RGB565)
{
LCD_SetDevFunc(i, LCD_DEVFUNC_DRAWBMP_16BPP, (void(*)(void))_LCD_DrawBitmap16bpp); // Set up drawing routine for 16bpp bitmap using DMA2D. Makes only sense with RGB565
}
//
// Set up drawing routine for 32bpp bitmap using DMA2D
//
if (_GetPixelformat(i) == LTDC_Pixelformat_ARGB8888)
{
LCD_SetDevFunc(i, LCD_DEVFUNC_DRAWBMP_32BPP, (void(*)(void))_LCD_DrawBitmap32bpp); // Set up drawing routine for 16bpp bitmap using DMA2D. Makes only sense with RGB565
}
//
// Set up custom color conversion using DMA2D, works only for direct color modes because of missing LUT for DMA2D destination
//
GUICC_M1555I_SetCustColorConv(_Color2IndexBulk_M1555I_DMA2D, _Index2ColorBulk_M1555I_DMA2D); // Set up custom bulk color conversion using DMA2D for ARGB1555
GUICC_M565_SetCustColorConv (_Color2IndexBulk_M565_DMA2D, _Index2ColorBulk_M565_DMA2D); // Set up custom bulk color conversion using DMA2D for RGB565
GUICC_M4444I_SetCustColorConv(_Color2IndexBulk_M4444I_DMA2D, _Index2ColorBulk_M4444I_DMA2D); // Set up custom bulk color conversion using DMA2D for ARGB4444
GUICC_M888_SetCustColorConv (_Color2IndexBulk_M888_DMA2D, _Index2ColorBulk_M888_DMA2D); // Set up custom bulk color conversion using DMA2D for RGB888
GUICC_M8888I_SetCustColorConv(_Color2IndexBulk_M8888I_DMA2D, _Index2ColorBulk_M8888I_DMA2D); // Set up custom bulk color conversion using DMA2D for ARGB8888
//
// Set up custom alpha blending function using DMA2D
//
GUI_SetFuncAlphaBlending(_DMA_AlphaBlending);
//
// Set up custom function for translating a bitmap palette into index values.
// Required to load a bitmap palette into DMA2D CLUT in case of a 8bpp indexed bitmap
//
GUI_SetFuncGetpPalConvTable(_LCD_GetpPalConvTable);
//
// Set up a custom function for mixing up single colors using DMA2D
//
#if emWin_Optimize
GUI_SetFuncMixColors(_DMA_MixColors);
#endif
//
// Set up a custom function for mixing up arrays of colors using DMA2D
//
GUI_SetFuncMixColorsBulk(_LCD_MixColorsBulk);
}
}
(4.5)GUI_X.c文件修改:
//该文件内主要定义了2个时间函数,其中的GUI_X_Delay是开发STemWin程序时常用的延迟函数
volatile GUI_TIMER_TIME OS_TimeMS;
/*********************************************************************
*
* Timing:
* GUI_X_GetTime()
* GUI_X_Delay(int)
Some timing dependent routines require a GetTime
and delay function. Default time unit (tick), normally is
1 ms.
*/
GUI_TIMER_TIME GUI_X_GetTime(void) {
return OS_TimeMS;
}
void GUI_X_Delay(int ms) {
int tEnd = OS_TimeMS + ms;
while ((tEnd - OS_TimeMS) > 0);
}
(4.6)GUI_X_Touch_Analog.c文件修改:
//该文件内定义了4个用于STemWin触摸屏函数调用的函数:
void GUI_TOUCH_X_ActivateX(void)
{
}
void GUI_TOUCH_X_ActivateY(void)
{
}
int GUI_TOUCH_X_MeasureX(void)
{
int ret = ...;
return ret;
}
int yc = 0;
int GUI_TOUCH_X_MeasureY(void)
{
int ret = ...;
return ret;
}
(5)创建一个窗口测试
(5.1)使用GUIBuilder.exe创建windows框架代码
STemWin的框架代码跟VisualStudio的win32窗体应用程序的框架代码几乎一摸一样,如果对VS的win32窗体应用开发熟悉的话,可以手工编写windows框架代码。
STemWin本身提供了一个工具软件GUIBuilder.exe,通过该软件可以创建windows框架代码文件。
前文提到的STemWin压缩包中,位于Software目录下,如图所示:

双击GUIBuilder.exe后打开界面如下:

如图,创建一个窗体,然后往窗体上拖一个按钮,保存之后可以得到文件WndMainDLG.c,其内容如下,是不是和win32窗体程序很像,我们只需要在_cbDialog函数中编写相关的消息处理函数就可以了:
/*********************************************************************
* *
* SEGGER Microcontroller GmbH & Co. KG *
* Solutions for real time microcontroller applications *
* *
**********************************************************************
* *
* C-file generated by: *
* *
* GUI_Builder for emWin version 5.32 *
* Compiled Oct 8 2015, 11:59:02 *
* (c) 2015 Segger Microcontroller GmbH & Co. KG *
* *
**********************************************************************
* *
* Internet: www.segger.com Support: support@segger.com *
* *
**********************************************************************
*/
// USER START (Optionally insert additional includes)
// USER END
#include "DIALOG.h"
/*********************************************************************
*
* Defines
*
**********************************************************************
*/
#define ID_WINDOW_0 (GUI_ID_USER + 0x00)
#define ID_BUTTON_0 (GUI_ID_USER + 0x01)
// USER START (Optionally insert additional defines)
// USER END
/*********************************************************************
*
* Static data
*
**********************************************************************
*/
// USER START (Optionally insert additional static data)
// USER END
/*********************************************************************
*
* _aDialogCreate
*/
static const GUI_WIDGET_CREATE_INFO _aDialogCreate[] = {
{ WINDOW_CreateIndirect, "Window", ID_WINDOW_0, 0, 0, 320, 240, 0, 0x0, 0 },
{ BUTTON_CreateIndirect, "Button", ID_BUTTON_0, 73, 53, 179, 76, 0, 0x0, 0 },
// USER START (Optionally insert additional widgets)
// USER END
};
/*********************************************************************
*
* Static code
*
**********************************************************************
*/
// USER START (Optionally insert additional static code)
// USER END
/*********************************************************************
*
* _cbDialog
*/
static void _cbDialog(WM_MESSAGE * pMsg) {
WM_HWIN hItem;
int NCode;
int Id;
// USER START (Optionally insert additional variables)
// USER END
switch (pMsg->MsgId) {
case WM_INIT_DIALOG:
//
// Initialization of 'Button'
//
hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0);
BUTTON_SetText(hItem, "Push Me");
// USER START (Optionally insert additional code for further widget initialization)
// USER END
break;
case WM_NOTIFY_PARENT:
Id = WM_GetId(pMsg->hWinSrc);
NCode = pMsg->Data.v;
switch(Id) {
case ID_BUTTON_0: // Notifications sent by 'Button'
switch(NCode) {
case WM_NOTIFICATION_CLICKED:
// USER START (Optionally insert code for reacting on notification message)
// USER END
break;
case WM_NOTIFICATION_RELEASED:
// USER START (Optionally insert code for reacting on notification message)
// USER END
break;
// USER START (Optionally insert additional code for further notification handling)
// USER END
}
break;
// USER START (Optionally insert additional code for further Ids)
// USER END
}
break;
// USER START (Optionally insert additional message handling)
// USER END
default:
WM_DefaultProc(pMsg);
break;
}
}
/*********************************************************************
*
* Public code
*
**********************************************************************
*/
/*********************************************************************
*
* CreateWindow
*/
WM_HWIN CreateWindow(void);
WM_HWIN CreateWindow(void) {
WM_HWIN hWin;
hWin = GUI_CreateDialogBox(_aDialogCreate, GUI_COUNTOF(_aDialogCreate), _cbDialog, WM_HBKWIN, 0, 0);
return hWin;
}
// USER START (Optionally insert additional public code)
// USER END
/*************************** End of file ****************************/
(5.2)将windows框架代码拷贝到uVision5项目
将WndMainDLG.c拷贝到uVision5项目中,并创建Widgets.h文件,如图:

将创建窗体的函数封装,写到Widgets.h中,以方便统一使用:
#include "DIALOG.h"
WM_HWIN CreateWindow(void);
(5.3)编写代码显示窗体
//10ms定时器,定时刷新触摸屏触摸状态
void set_touch_detect_timer()
{
//设置中断
NVIC_InitTypeDef nvic_tim;
nvic_tim.NVIC_IRQChannel = TIM2_IRQn;
nvic_tim.NVIC_IRQChannelPreemptionPriority = 1;
nvic_tim.NVIC_IRQChannelSubPriority = 2;
nvic_tim.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_tim);
//配置Tim
TIM_TimeBaseInitTypeDef tim_init;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM7时钟,即内部时钟CK_INT=72M(STM32F103RC的芯片)
TIM_Cmd(TIM2, DISABLE); //配置前,先关闭计数器
tim_init.TIM_Prescaler = 72 -1;// 预分频器数值,16位; 计数器的时钟频率CK_CNT等于f CK_PSC /(PSC[15:0]+1)。在每一次更新事件时,PSC的数值被传送到实际的预分频寄存器中。
tim_init.TIM_Period = 10000;// 自动重装载数值,16位; 即多少个脉冲产生一个更新或中断(1周期)。如果自动重装载数值为0,则计数器停止。
TIM_TimeBaseInit(TIM2, &tim_init); // 初始化定时器
TIM_ClearFlag(TIM2, TIM_FLAG_Update);// 清除计数器中断标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 开启更新中断(只是开启功能,未工作)
TIM_Cmd(TIM2, ENABLE);// 是否使能计数器(使能了就会立即开始工作)
}
void TIM2_IRQHandler(void)
{
GUI_TOUCH_Exec();//必须,10ms刷新一次触摸屏触摸状态
}
int main(void)
{
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
systick_delay_us(100000);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//初始化内存、显示屏和触摸屏驱动
SDRAM_Init();
TFTLCD_Init();
tp_dev.init();
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_CRC, ENABLE);
WM_SetCreateFlags(WM_CF_MEMDEV);
GUI_Init();
GUI_Clear();
GUI_CURSOR_Show();
//开启定时检测触摸屏的时钟
set_touch_detect_timer();
//创建并显示窗体
WM_HWIN h = CreateMainWnd();
while(1)
{
GUI_Exec();//必须,刷新窗体
}
}
至此,STemWin移植完毕,可以看到效果如图:

您的打赏是我写作的动力!