简介:面向N9H30嵌入式平台的emWin非操作系统BSP资源,适合在裸机环境下构建图形界面的嵌入式工程师,以及希望系统学习ARM9与emWin组合开发的进阶用户。非OS架构不依赖RTOS,界面刷新与输入处理需通过底层驱动和主循环调度完成,BSP包为此提供了可直接参考的工程骨架。包内围绕emWin图形库给出完整源码体系,包含库文件、H头文件、GUIDEMO系列示例、多国语言编码转换支持以及窗口控件相关实现,可帮助开发者快速移植LCD驱动、创建交互界面并理解非OS模式下的GUI事件流程。资源共1030个文件,以C源文件和H头文件为主,辅以HTML文档、工程配置文件、PDF手册和少量资源表,压缩包约29.44MB,目录模块划分明确,便于按功能检索。目前已有365人学习下载,适合正在调试N9H30显示方案、需要参考官方BSP代码或准备从零搭建emWin工程的开发者直接使用。 做嵌入式人机界面这块,我跟 emWin 打交道的年头不算短了。前两年接手一个工业仪表项目,主控选的是新唐 N9H30,需求很明确:要跑图形界面,但系统又不复杂,没必要上 Linux 或者 RTOS,于是就用上了 N9H30 的 emWin 非操作系统 BSP。这套 BSP 的全称是 N9H30_emWin_NonOS,本质上就是官方把 N9H30 的外设驱动、启动代码和 emWin 图形库整合成一个可以在裸机环境下直接跑的工程模板。这篇文章就把我基于这套 BSP 做产品时的经验整理出来,包括整体结构、移植细节、主循环写法,以及我踩过的坑。适合正在评估 N9H30、或者想在 ARM9 平台上用 emWin 做界面但又不确定怎么起手的工程师。
1. 项目概述与整体方案选型
1.1 这套 BSP 到底解决了什么问题
在 N9H30 上做 UI,摆在面前的有几条路:
- 跑 Linux + Qt/GTK:功能强大,但硬件要求高,启动复杂,开发周期长;
- FreeRTOS/RT-Thread + emWin/LVGL:支持多任务,但要做好资源管理和同步,复杂度上了一个台阶;
- NonOS + emWin:一个 super loop 解决所有事,代码量小,行为可预期,稳定性容易保障。
对于工控仪表这种界面层级简单、逻辑不特别多的产品,NonOS + emWin 其实是最平衡的方案。N9H30 本身是 ARM926EJ-S 内核,主频可以跑到 300MHz,内置大容量 DDR,跑 emWin 做 800×480 甚至更高的分辨率都没问题;而且这颗料自带 LCD 控制器,直接支持 TFT RGB 接口,外接触摸屏很方便。BSP 的意义就在于把这颗芯片的内外设——比如时钟、GPIO、串口、LCD 控制器、触摸接口、NAND/SD 启动等——全部初始化好,把 emWin 和硬件的对接层(也就是驱动层)写好,让你不用从寄存器开始一点点抠。
其实说白了,BSP 就是“让工程先跑起来”的最短路径。没有 BSP,你可能要花两周梳理数据手册;有了 BSP,你花两小时看懂框架,就能进入业务代码开发。这个 N9H30_emWin_NonOS 工程尤其适合三种人:一是刚接触 ARM9 的嵌入式新手,想快速看到 emWin 界面效果;二是项目评估阶段,想验证 N9H30 这颗料能不能满足 UI 性能要求;三是做产品化开发,需要一个干净可靠的基础工程来继续叠功能。
1.2 为什么选 NonOS 而不是直接上 RTOS
我见过很多团队,一上来就写 RTOS,结果业务逻辑本身就二三十个函数,写成任务后反而被信号量、消息队列绊住。这里不是否定 RTOS,而是要根据场景选型。NonOS 的核心是死循环加定时器轮询,所有事情按优先级在循环里排队执行。优点是:
- 没有任务调度开销,行为完全确定,方便调试;
- 没有任务切换和资源互斥的复杂概念,栈和堆的分配一目了然;
- 代码直观,同事接手成本低。
缺点是实时性靠程序员的“良心”,如果一个函数占用太长时间,其他事情会被卡住。所以在 NonOS + emWin 的方案里,UI 刷新、触摸扫描、串口通信这些要合理地“切碎”放进主循环。至于 emWin,它在 NonOS 环境下不需要额外加锁机制,GUI_Exec()、GUI_Delay() 按逻辑顺序执行,比 RTOS 下的移植还要省事。这也是这个 BSP 存在的重要理由。
2. 环境搭建与 BSP 工程结构剖析
2.1 开发环境与烧录流程
N9H30 的官方 BSP 工程一般用 Keil MDK 打开,工程文件是 .uvprojx。新唐还提供 NuWriter 烧录工具,用来把编译出来的 bin 文件烧到 NAND Flash 或 SD 卡。我的习惯是开发阶段用 SD 卡启动,省去反复擦写 NAND 的等待;等代码稳定了再通过 NuWriter 烧进 NAND,产品量产用 NAND 启动更稳妥。
开发环境有几点要留意:Keil 建议用较新的版本,旧版本对 ARM926EJ-S 的支持不够完善;工具链用 ARMCC 或者 AC6 都行,浮点支持按项目需要配置;下载算法和烧录脚本直接用新唐提供的,不建议自己写,踩坑成本高。如果你用 GCC 工具链,官方也提供 Makefile 版本,不过 Keil 下调试体验更好,配合 J-Link 可以很方便地打断点、查寄存器。
2.2 目录结构与启动流程
下面是一个典型的 N9H30_emWin_NonOS 工程目录,不同版本可能略有差异:
N9H30_emWin_NonOS/ ├── Library/ │ ├── BSP/ # 外设驱动:UART、GPIO、LCD、定时器等 │ ├── emWin/ # emWin 库、头文件、配置模板 │ └── Startup/ # 启动代码、向量表 ├── Sample/ │ └── NonOS_emWin_Demo/ │ ├── main.c # 主函数、超级循环 │ ├── LCDConf.c # LCD 控制器参数与底层驱动 │ ├── GUI_X_Config.c # emWin 内存池配置 │ ├── GUITouchConf.c # 触摸配置(如果用触摸) │ └── 工程文件 └── NuWriter/启动流程可以简单理解为三步:芯片上电后,N9H30 内部的 Boot ROM 根据启动引脚设置,从 NAND、SD 或 EBI 加载代码到 DDR 并跳转;随后 Startup 汇编代码完成堆栈初始化、RW/ZI 段拷贝与清零;最后进入 C 语言的 main,在 main 里执行 BSP_Initialization() 完成外设时钟、GPIO、LCD 控制器的初始化,再调用 GUI_Init() 启动 emWin。理解这条链路对你排查“上电白屏”“跑飞卡死”之类的问题是很有用的。
3. emWin 移植的核心细节
3.1 LCD 控制器配置:显存、时序与颜色格式
在 NonOS 环境下,LCD 驱动本质上是把 N9H30 的 LCD 控制器配置成 RGB TFT 模式,然后把 framebuffer 地址交给控制器,让它持续刷新。emWin 这一侧的对接,关键文件是 LCDConf.c。你需要确认三个核心参数:
- 分辨率:比如 800×480 或 1024×768;
- 颜色格式:最常见的是 RGB565(16bpp),也可以配 RGB888(24bpp);
- 显存地址:N9H30 内核访问内存很直接,显存就是一段普通 DDR 地址,但要注意地址对齐。
以 800×480 RGB565 为例,一帧显存大小是 800×480×2=768000 字节,也就是 750KB。N9H30 内置的 DDR 有几十 MB,完全不是瓶颈,你也可以开双缓冲(double buffer)来避免撕裂,代价是再多占 750KB。实际项目中我通常建议开双缓冲,配合 emWin 的存储设备特性,界面会明显更丝滑。
典型 LCDConf 配置流程:
void LCD_X_Config(void) { GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0); LCD_SetSizeEx(0, 800, 480); LCD_SetVSizeEx(0, 800, 480); // 设置显存地址 // 初始化 N9H30 LCD 控制器的时钟、像素时钟、时序参数 }这里有一个很容易漏掉的细节:N9H30 的 LCD 控制器对水平和垂直方向的 front porch、back porch、sync polarity 非常敏感,这些参数必须和你手上屏的 datasheet 完全一致,否则轻则显示偏移,重则花屏或黑屏。我建议上电后先不运行任何应用代码,直接用纯色刷屏测试,确认颜色和边界都正确,再去做 UI 逻辑。
3.2 NonOS 下的时间基线与定时
emWin 内部有大量逻辑依赖系统时间,比如动画、光标闪烁、GUI_Delay。在 NonOS 环境里,你得自己提供一个毫秒级的 tick。N9H30 的 BSP 一般用某个定时器或者系统节拍中断来维护一个全局的 tick 计数。
对应到 emWin 的移植层,主要实现两个函数:
unsigned int GUI_X_GetTime(void) { return (unsigned int)tick_count; } void GUI_X_Delay(int ms) { unsigned int end = tick_count + ms; while ((int)(tick_count - end) < 0) { // 可以在这里处理触摸扫描或其他短任务 } }很多人第一次跑 emWin 在 NonOS 下出现“界面不刷新”“点击没反应”,问题往往就是 tick 没起来,GUI_Delay 陷入死循环。另一个细节:GUI_Delay 在 NonOS 下是阻塞的,如果你主循环里同时要响应触摸或通信,不要把延时时间设太长,通常 10~20ms 一档就够了,否则会感觉到明显的输入延迟。
3.3 触摸输入:从原始坐标到 GUI 坐标
N9H30 的 BSP 支持的触摸屏类型一般是 SPI 接口的电阻屏(如 ADS7846/XPT2046)或 I2C 接口的电容屏(如 GT9xx 系列)。emWin 侧的工作比较简单:在 GUITouchConf.c 里提供一个底层函数,周期性读取触摸控制器,把原始坐标转换成 0~1023 或 0~4095 的值交给 emWin,emWin 再根据校准参数映射到 LCD 坐标。
电阻屏校准是很多人栽跟头的地方。上电后的坐标和显示坐标常常有旋转、缩放和偏移,原因是触摸屏的 ADC 坐标轴和 LCD 的坐标轴方向未必一致,需要在代码里做线性变换。我一般用 5 点校准,取左上、右上、右下、左下、中心五个点,算出 6 个仿射变换系数,再用软件滤波去掉毛刺。电容屏虽然出厂有算法,但 N9H30 通过 I2C 接电容屏时,上报的是归一化坐标,反而简单一些,主要注意中断引脚和 I2C 时钟频率。
3.4 内存池配置与存储设备使用
emWin 在 NonOS 下需要一个内存池,用来分配窗口对象、字体、存储设备等。这个内存池的大小在 GUI_X_Config.c 里配置:
#define GUI_NUMBYTES (1024 * 1024) /* 1MB RAM for emWin */ static U32 aMemory[GUI_NUMBYTES / 4]; void GUI_X_Config(void) { GUI_ALLOC_AssignMemory(aMemory, GUI_NUMBYTES); // 其他初始化 }对于 800×480 的界面,我建议至少给 emWin 分配 512KB 到 1MB。如果做复杂界面,后面可能还要开存储设备来做纹理缓存,内存太紧张会导致 GUI_Init 后界面控件创建失败,表现是程序不报错但不显示控件。这种问题最难排查,我一般会在代码里加上对 API 返回值的检查,特别是 WM_CreateWindow、GUI_MEMDEV_Create 这类容易静默失败的调用。
4. 实操:从零跑通一个 emWin 界面
4.1 main 函数的标准写法
N9H30_emWin_NonOS 工程里,main 的框架基本是固定的,下面是我实际用的模板:
#include "BSP.h" #include "GUI.h" volatile unsigned int tick_count = 0; int main(void) { BSP_Initialization(); // 时钟、GPIO、LCD、触摸、串口等 GUI_Init(); // emWin 初始化 Gui_ShowMainPage(); // 创建界面(对话框/窗口/控件) while (1) { GUI_Exec(); // 处理窗口消息和重绘请求 GUI_Delay(20); // 延时并让出 CPU Touch_Process(); // 触摸扫描与坐标上报 Serial_Process(); // 串口数据处理 } }这段代码有几个要点需要说明。第一,GUI_Exec() 不能省,窗口系统模式下重绘和消息传递都靠它驱动;如果只是做简单的画点画线不建窗口,可以少调它,但一旦用了控件就必须周期性调用。第二,GUI_Delay(20) 既实现了定时刷新,又相当于给其他任务留出执行窗口,所以一般放在循环末尾而不是开头,避免延时分走了 CPU 时间。第三,一些耗时处理(比如协议解析)尽量拆成小片段,防止界面卡顿。
4.2 创建页面与控件的常用套路
我习惯用窗口加对话框的方式组织页面。简单的状态页,可以直接在 main 里创建窗口,添加 TEXT 和 IMAGE 控件;复杂一点的二级菜单页,建议用对话框(GUIBuilder 生成代码)来做,后续维护方便很多。给出一个极简示例:
void Page_Init(void) { WM_HWIN hWin = GUI_CreateDialogBox(_aDialogCreate, GUI_COUNTOF(_aDialogCreate), _cbDialog, 0, 0, 0); if (hWin == 0) { // 创建失败,多半是内存池不够或资源ID重复 } }回调函数 _cbDialog 中处理 WM_NOTIFY_PARENT 之类的消息,比如按下按钮切换页面、更新数据文本等。如果数据更新频繁,推荐用 WM_SetCallback 局部回调或者自定义消息,这样避免每次重绘整个界面,性能差别在工业仪表上体感很明显。
4.3 性能优化的几个实操技巧
在 N9H30 上跑 emWin,性能瓶颈通常不在 CPU 主频,而在 framebuffer 的写入方式和无效重绘。我总结几个实用技巧:
- 优先使用 16bpp RGB565,不要图省事上 24bpp,带宽差一半;
- 大图用调色板格式或压缩处理,尤其是图标资源,能省内存还能拖快刷新速度;
- 用存储设备(MEMDEV)把静态背景预渲染,动态内容再用 WM_InvalidateWindow 局部刷新;
- 如果 N9H30 的 LCD 控制器带 2D 加速能力,可以把 BitBlt 类操作下放到驱动层,emWin 的模板驱动支持这种接口。
还有一个容易被忽略的点:非操作系统环境下,如果 CPU 主频跑在低功耗模式,LCD 控制器刷新帧率会有明显波动,表现为界面闪烁。我在这个项目上就吃过亏,后来把 CPU 频率和 DDR 频率固定到性能档,问题才消失。排查这类问题,逻辑分析仪量一下 LCD 像素时钟最直接。
5. 常见问题与排查技巧实录
5.1 高频故障速查表
我把在 N9H30_emWin_NonOS 项目里遇到的高频问题整理成了表,对号入座排查会快很多:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电白屏/无显示 | LCD 初始化失败、显存地址不对、背光未点亮 | 确认 main 里 LCD_Init 是否执行;查显存地址是否在有效 DDR 范围;万用表量背光使能 |
| 显示整体偏移 | LCD 时序参数(HBP/VBP)错误 | 对照屏 datasheet 核对 front/back porch |
| 颜色不对,红蓝交换 | RGB 格式 RGB565 与 RGB888 配置错位 | 检查 GUICC_M565 与 LCD 控制器颜色位定义 |
| 界面卡死 | GUI_Delay 使用的 tick 中断未开启 | 检查定时器初始化,确认 tick_count 是否在增加 |
| 控件创建但不显示 | 内存池太小 | 加大 GUI_NUMBYTES,检查创建返回值 |
| 触摸点击偏移大 | 校准系数错误 | 重新做 5 点校准,检查 ADC 采样滤波 |
| 画面撕裂/闪烁 | 单缓存 + 频繁整屏刷新 | 改双缓冲,配合 MEMDEV 预渲染 |
这个表格基本覆盖了从硬件到软件的大部分典型问题。另外,如果你改动了分辨率,记得把 BSP 里的 LCD_Timing 结构体参数同步更新,很多时候“看着是软件问题,实际是时序参数没改”。
5.2 我的几个独家排查思路
第一,遇到“花屏”别只盯代码,先检查物理接线和电源。之前有个同事折腾了两天显示乱码,最后发现是 LCD 排线长了 20cm,信号衰减加干扰,换短排线就好了。ARM9 虽然是低价方案,但像素时钟到几十 MHz 后,布线质量对显示的影响非常明显。
第二,用“二分法”缩小问题范围。如果 emWin 界面显示异常,先用 BSP 自带的纯色或渐变测试函数刷屏,排除 emWin 和 LCD 驱动问题后,再逐步往上加控件和窗口。很多时候问题并不在 emWin,而在更底层的 framebuffer 地址覆盖——比如你在 NonOS 下把某个数组定义到了显存地址附近,溢出时正好把显示数据写乱了。
第三,调试时多用断言和返回值检查。NonOS 环境里没有 OS 帮你查内存越界,我给 BSP 加了一个简单的 canary 检测,每轮循环检查关键的栈边界,在产品发布前再关掉。这种方式比事后看崩溃现象高效得多。
最后说点个人体会。我用 N9H30_emWin_NonOS 完整交付过两个项目,一个工业仪表,一个充电桩交互面板。回头来看,这套方案的最大价值就在于“简单可控”:一个主循环、一套图形库、一层干净的外设驱动,出了问题你能顺着逻辑一直看到寄存器级别。对比后来用 Linux 或 RTOS 的方案,它的开发确定性和排障速度反而更高。如果你刚拿到这个 BSP,我的建议是先不急着写业务代码,把上面提到的底层配置一项项验证过去,尤其是 LCD 时序和 tick 中断,这两块稳定了,后面的 UI 开发基本就是水到渠成的事。希望这篇记录能帮你少走一些弯路。
本文还有配套的精品资源,点击获取