emWin 入门实战:从零开始点亮你的第一个图形界面
你有没有遇到过这样的场景?项目需要一块显示屏,原本打算用数码管或段码屏凑合一下,结果产品经理甩过来一张高颜值的UI设计图:“我们要做交互体验一流的设备。”——这时候,你就知道,是时候上图形界面了。
在嵌入式世界里,emWin就是那个默默支撑起无数工业设备、医疗仪器和智能家电屏幕背后的“老练匠人”。它不花哨,但足够稳;资源吃得少,干活却利索。今天,我们就抛开复杂的理论堆砌,手把手带你用 STM32 驱动一块 TFT 屏,跑出第一个 emWin 界面——就从Hello emWin!开始。
为什么选 emWin?不是还有 LVGL 吗?
市面上 GUI 框架不少,LVGL 火得发紫,GitHub 星标几万,社区热闹非凡。那为啥还要学 emWin?
答案很简单:稳定、成熟、文档全、企业级项目压得住场子。
- LVGL像是个充满创意的年轻人,功能炫酷,动画丝滑,适合快速原型开发。
- emWin则更像一位经验丰富的工程师,代码结构清晰,运行效率高,出了问题查手册就能定位,特别适合对长期维护有要求的产品。
更重要的是,emWin 可以完全在裸机(无操作系统)环境下运行,这对很多成本敏感、资源紧张的小型控制系统来说,简直是福音。
而且别忘了,SEGGER 官方提供的文档之详尽,在嵌入式圈子里几乎找不到对手。每个函数都有说明,每个配置项都有解释,连“为什么会闪屏”这种问题都写进了 FAQ。
所以,如果你想做的不是一个玩具 Demo,而是一个能出厂、能量产、十年后还能修得了的系统,emWin 值得你认真对待。
准备工作:硬件平台与工具链
我们以最常见的组合为例:
- MCU:STM32F407ZGT6(自带 FSMC 接口)
- 屏幕:3.5 寸 TFT LCD,分辨率 320×240,驱动芯片 ILI9341
- 接口方式:FSMC 8080 并行模式(速度快,帧率更有保障)
- 开发环境:Keil MDK-ARM v5.x
- GUI 库版本:emWin 6.32(评估版,免费使用,带 SEGGER Logo)
✅ 提示:即使你用的是 SPI 接口屏幕或者别的 MCU(比如 STM32H7、GD32),只要底层驱动适配好,后续流程基本一致。
你需要提前准备好:
- STM32CubeMX 工程(配置时钟、FSMC、GPIO)
- ILI9341 的初始化代码(参考 Adafruit 或正点原子开源代码)
- emWin 官网注册账号后下载 Evaluation Package
第一步:把 emWin 源码塞进工程
emWin 不是 HAL 那种头文件+库文件的形式,它是纯源码交付,意味着你可以看到每一行实现逻辑。
解压 emWin 包后,你会看到几个关键目录:
| 目录 | 作用 |
|---|---|
/Core | 核心绘图引擎,必须加入 |
/Config | 配置文件模板,要复制到项目中修改 |
/Widget | 按钮、滑条等控件实现 |
/MemoryDevice | 内存设备支持(防闪烁利器) |
操作步骤如下:
- 在你的 Keil 工程中新建分组
emWin/Core,emWin/Widget等; - 把对应
.c文件添加进去(建议先只加 Core 和基础 Widget); - 头文件路径添加到
Include Paths中; - 编译,确保没有语法错误。
⚠️ 注意:emWin 源码默认包含大量未使用的模块。如果你直接全加进来,可能会因为内存不足链接失败。建议按需启用。
第二步:告诉 emWin 屏幕长什么样 ——LCDConf.h
这是你和 emWin 的第一份“协议”,告诉它你的屏幕有多大、颜色怎么表示、如何写像素。
// LCDConf.h #ifndef LCDCONF_H #define LCDCONF_H #define LCD_XSIZE (320) #define LCD_YSIZE (240) #define LCD_BITSPERPIXEL (16) // RGB565 #define LCD_CONTROLLER (-1) // 自定义控制器 #define LCD_SWAP_RB (1) // ILI9341 需要交换 R/B 通道 #endif这几个宏非常重要:
LCD_XSIZE/Y_SIZE必须和实际物理分辨率一致;LCD_BITSPERPIXEL=16表示每像素占 2 字节,即 RGB565 格式;LCD_SWAP_RB=1是因为 ILI9341 数据格式是 BGR565,如果不交换,红色会变成蓝色!
这个文件还会被 emWin 内部 include,所以一定要放在编译路径里。
第三步:实现两个底层函数,打通“任督二脉”
emWin 不关心你是用 FSMC 还是 SPI 驱动屏幕,它只需要你能完成两件事:
- 初始化屏幕;
- 能在指定坐标画一个像素。
这就靠下面这两个函数来实现。
1. 像素绘制函数:LCD_L0_SetPixelIndex
void LCD_L0_SetPixelIndex(int x, int y, int ColorIndex) { if (x >= LCD_XSIZE || y >= LCD_YSIZE || x < 0 || y < 0) return; LCD_DrawPixel(x, y, ColorIndex); }这里的LCD_DrawPixel()是你自己写的硬件驱动函数,负责通过 FSMC 写命令/数据寄存器更新显存。
注意:ColorIndex 是调色板索引值,但在 16bpp 模式下通常直接当作 RGB565 颜色值处理。
2. 显示控制器初始化:LCD_X_DisplayDriver
这是一个消息分发函数,emWin 会通过它通知你做一些事,比如初始化、刷新区域等。
int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void *p) { switch (Cmd) { case LCD_X_INITCONTROLLER: LCD_Init(); // 调用你的 ILI9341 初始化函数 return 0; default: return -1; } }目前我们只处理LCD_X_INITCONTROLLER消息,其他先忽略即可。
🔍 小知识:如果你以后要做双图层或多缓冲,这里就是扩展点。
第四步:写 main 函数,让屏幕“活”起来
现在所有准备工作就绪,让我们来写主程序。
#include "stm32f4xx_hal.h" #include "GUI.h" #include "WM.h" extern void LCD_Init(void); // 你的 LCD 初始化函数 int main(void) { HAL_Init(); SystemClock_Config(); // 设置 168MHz 主频 MX_GPIO_Init(); // 初始化 LCD 硬件 LCD_Init(); // 启动 emWin GUI_Init(); // 设置背景为蓝色,清屏 GUI_SetBkColor(GUI_BLUE); GUI_Clear(); // 设置白色文字,显示大标题 GUI_SetColor(GUI_WHITE); GUI_SetFont(&GUI_Font32_ASCII); GUI_DispStringAt("Hello emWin!", 80, 50); // 创建一个按钮 BUTTON_Handle hBtn = BUTTON_CreateEx(100, 120, 120, 40, WM_HBKWIN, 0, 0, "Click Me"); // 主循环 while (1) { GUI_Delay(50); // 给 emWin 留出时间处理内部任务 } }就这么几十行代码,只要你硬件驱动没问题,烧进去之后,屏幕上就会出现:
✅ 蓝色背景
✅ 白色大字 “Hello emWin!”
✅ 一个写着 “Click Me” 的灰色按钮
恭喜!你的第一个 emWin 界面已经跑起来了!
常见坑点与调试秘籍
别高兴太早,新手最容易栽在这几个地方:
❌ 屏幕全白或全黑?
- 检查
LCD_Init()是否正确发送了初始化序列; - FSMC 地址线是否接对?尤其是 A16/A18 控制 RS 引脚;
- ILI9341 的复位引脚有没有拉低再释放?
❌ 文字显示乱码或偏移?
- 检查字体指针是否正确:
&GUI_Font32_ASCII是标准内置字体; - 确保
GUI_Init()已调用,否则字体系统未初始化。
❌ 按钮点击没反应?
- 当前代码中没有启用消息循环处理!
emWin 的事件机制依赖WM_Exec()或定时轮询。
解决办法:加入简单的消息处理:
while (1) { WM_Exec(); // 处理窗口消息 GUI_Delay(20); // 防止 CPU 占满 }这样按钮才能响应触摸或按键输入。
如何让它真正“交互”起来?
现在的界面只是静态展示。要让它动起来,你需要接入输入设备。
方案一:电阻触摸屏(XPT2046)
- 使用 SPI 读取 ADC 值;
- 调用
GUI_TOUCH_StoreStateEx(x, y, Pressed)上报坐标; - emWin 自动将触摸事件转发给按钮、滑块等控件。
方案二:外部中断 + 按键模拟
- 把物理按键映射成虚拟鼠标点击;
- 在中断中调用
GUI_MOUSE_StoreState()。
一旦有了输入,你就可以实现:
- 页面切换
- 参数调节
- 实时数据显示
性能优化技巧:别让界面卡成幻灯片
emWin 很高效,但也经不起瞎折腾。以下是几个实用建议:
✅ 开启内存设备(GUI_MEMDEV)
避免频繁局部刷新导致的闪烁:
int Device = GUI_MEMDEV_Open(0, 0, 320, 240); GUI_MEMDEV_Select(Device); // 在内存中绘图... GUI_MEMDEV_CopyToLCD(); // 一次性刷到屏幕 GUI_MEMDEV_Close(); GUI_MEMDEV_Select(0);✅ 使用双缓冲(GUI_MULTIBUF)
配合GUI_MULTIBUF_Begin()/End()实现平滑动画,防止撕裂。
✅ 裁剪功能模块
在GUIConf.h中关闭不用的功能:
#define GUI_WINSUPPORT 0 // 不用窗口管理时关闭 #define GUI_SUPPORT_MEMDEV 1 // 按需开启 #define GUI_VNC_SUPPORT 0 // 调试用,发布时关掉这能显著减少代码体积和 RAM 占用。
中文显示怎么做?
emWin 默认只支持 ASCII,想显示“设置”、“菜单”怎么办?
答案是:自己生成中文字体数组。
SEGGER 提供了一个叫FontCvt的工具(Windows 下运行),可以将 TrueType 字体导出为 C 数组。
步骤如下:
- 打开 FontCvt;
- 选择 SimHei.ttf 等中文字体;
- 设置字符集(如 GB2312 常用汉字);
- 导出为
GUI_FONT结构体; - 加入工程,调用
GUI_UseFont(&my_chinese_font)。
虽然麻烦一点,但一旦做好,中文显示稳定可靠,不怕乱码。
最后说几句掏心窝的话
很多人觉得 GUI 开发门槛高,其实不然。emWin 的学习曲线是前重后轻——前面移植、配置、适配底层确实费劲,但一旦跑通第一个界面,后面的控件使用、页面设计就像搭积木一样简单。
而且你会发现,一旦掌握了 emWin,你就掌握了嵌入式 GUI 的底层思维:
- 显存管理
- 消息机制
- 双缓冲原理
- 输入事件分发
这些思想在 TouchGFX、LVGL 甚至 Qt for MCUs 中都是相通的。
所以,哪怕你现在用的是 LVGL,了解 emWin 依然有价值——因为它教会你怎么“从零构建”一个 GUI 系统,而不是依赖框架帮你遮住所有细节。
如果你成功跑出了那个蓝色背景上的 “Hello emWin!”,不妨拍张照片留念。这不是简单的字符串打印,而是你迈向专业级 HMI 开发的第一步。
下一步,试试做一个带滑动条的亮度调节界面?或者用GRAPH控件画个实时波形图?
真正的 GUI 之旅,才刚刚开始。