
简介面向嵌入式开发者的新唐N9H30非操作系统板级支持包基于C语言实现集成emWin图形用户界面库适合在无裸机系统环境下进行图形交互应用开发常用于智能终端、工业人机界面等场景。压缩包内共1030个文件总大小约为29.44MB其中h头文件392个、c源文件330个共计700余个程序文件另含工程配置文件、网页说明文档、PDF手册、图片资源及静态库目录归类清晰方便项目导入与参考。包内提供emWin官方演示案例、中文字库支持模块、链接库以及N9H30平台相关配置有助于开发者掌握图形库驱动接入、控件使用和字库移植方法并能在此基础上快速搭建界面原型。已有368人学习下载对于从事新唐MCU界面开发的嵌入式工程师这是一份实用性很强的起步模板和底层参考。1. 在 N9H30 上跑 emWin 却不带操作系统这个 NonOS BSP 是什么N9H30_emWin_NonOS 这个名字看起来像一个仓库目录实际是一套跑在 N9H30 芯片上的 emWin 非操作系统 BSP。N9H30 是内置 DDR 和 LCD 控制器的 GUI 型 SoCemWin 是官方主推的图形库NonOS 意味着整个工程不跑 RTOS只有死循环加定时器中断UI 照样能转。去年有朋友做 7 寸 HMI一直担心裸机跑不了 emWin 的窗口动画我帮他搭了一套这样的 BSP 之后他才发现比带 RTOS 的版本还省心不用管任务优先级、不用写互斥量开机就是 GUI_Init 加 while(1)。这套 BSP 解决的核心问题是把 emWin 的底层接口接到 N9H30 的 LCD 控制器、触摸、DDR 和定时器上让“显示刷新”成为一个可持续交付的裸机工程。适合两类人一类是被 RTOS 调度问题困扰、想退回单循环的工程师另一类是刚接触 emWin、想直接看最小可跑工程的新手。新手能照着章节省略步骤搭环境熟手可以重点看第 5 章的坑——那些坑大多不在 emWin 里而在 BSP 和芯片本身的初始化细节上。2. 先拆 BSP 框架N9H30 裸机工程的六块拼图拿到任何 NonOS BSP先别急着改代码把工程里每个文件的角色摸清楚。N9H30 的裸机工程和带 RTOS 的版本有一个关键区别没有调度器替你调用外设驱动所有初始化都顺序写在 main 最前面emWin 的时钟和延时必须靠硬件定时器顶起来否则 GUI_Delay 直接躺平。先把依赖关系梳理清楚后面改动才不会东拉西扯。2.1 裸机启动必须按顺序完成四件事顺序反了就是花屏N9H30 上电后 CPU 可以立刻跑但 DDR、LCD 控制器和外部外设不会自己准备好。常见做法是固定按四步走先给 PLL 和系统时钟做倍频再初始化 DDR2 控制器并做稳定等待然后配置 LCD GPIO 复用和面板时序最后初始化触摸需要的 ADC 或 I2C。顺序为什么死板因为 LCD DMA 的帧缓冲在 DDR 里DDR 不稳LCD 控制器一开启就抓到空数据而 PLL 没配好DDR 频率就不合法。顺序反了的典型现象是全屏花点、开头白屏或者程序跑到 DDR 初始化之前直接 HardFault。void board_init(void) { system_clock_init(); /* 倍频CPU到300MHz同时配LCD像素时钟分频 */ ddr_init(); /* 按DDR手册写时序参数等待PLL稳定 */ lcd_pin_mux(); /* LCD数据/时钟/控制脚复用为RGB接口 */ lcd_controller_init(); /* 设置HSYNC/VSYNC/DEN/像素时钟 */ touch_adc_init(); /* ADC通道和采样周期供电阻屏读取 */ }这些函数在具体厂商 BSP 里可能叫 SystemClock_Init、BSP_DDR_Init 之类名字不一定一样但调用顺序千万别改。system_clock_init 里最核心的是 PLL 倍频系数和 LCDCLK 分频值先用外部晶振频率乘倍频数得到系统时钟再用系统时钟除分频数得到面板像素时钟。计算例子外部晶振 12MHz、目标 300MHz倍频数就是 25面板像素时钟典型值 20MHzLCDCLK 分频约 15。寄存器位宽每家不同直接查 N9H30 芯片手册的时钟寄存器说明不要照搬无关工程的参数。2.2 六块拼图文件职责与依赖关系文件/模块在工程里干什么要不要常改启动文件 startup.S堆栈初始化、中断向量表基本不动system_clock.c/hPLL、外设时钟使能和分频换屏/换晶振才改ddr_init.cDDR2 控制器时序和地址映射不用动lcd_drv.c面板时序、RGB 引脚、帧缓冲地址换屏必须改touch_drv.cADC 或触摸 IC 读写、坐标映射校准/换面板时改GUI_X.c GUIConf/LCDConfemWin 底层接口时间、延迟、内存、显示参数移植必改依赖关系有一条主线启动文件先跑之后是 system_clock再往后是 ddr最后才能碰 LCD 和触摸GUI_X 没有硬件依赖但必须保证在 GUI_Init 之前编译进来。我习惯把 board_init 和 GUI 相关初始化拆成两个函数board_init 将来可以直接复用到带 RTOS 的工程上层线程创建后再调 GUI_Init改动最小。还有一类文件是厂商出厂默认就配好的比如 dd r_init 里的时序参数表除非换了 DDR 颗粒型号否则不要去动它。2.3 移植前先定三件事否则返工量很大第一件事是颜色深度。RGB565 最常见显存占用减半LCD DMA 带宽压力也小RGB888 颜色正但带宽翻倍N9H30 的 CPU 访问 DDR 和 LCD 控制器刷新会互相抢总线。如果只做仪表盘、按钮和文字直接锁 RGB565要显示照片级图片再上 RGB888。第二件事是帧缓冲位置。NonOS 下没有 MMU所有地址都是物理地址一般把帧缓冲放在 DDR 里一块固定区域用宏比如 LCD_BASE 定义。第三件事是刷新路径emWin 在裸机上通常有两条路软件写点驱动或者走 2D 硬件加速的驱动。在 LCDConf.c 里配置 LCD_CONTROLLER 后画块操作会走硬件如果退回软件模板全部像素靠 CPU 写寄存器刷大图会明显变慢。这三个决定在工程初期就要落进 GUIConf 和 LCDConf不要等项目跑到一半再换颜色深度——字体、位图资源全部要重新生成帧缓冲地址也要挪。另一个容易被忽略的是窗口管理器 WM 的启停不用复杂控件可以省掉 WM内存占用能少一块大多数项目还是开 WM因为按钮、Slider、消息传递都靠它。开了 WMGUI_NUMBYTES 就得预留窗口对象和无效区列表的开销。综合看下来这些决策都属于“后改很痛”的类型先花半天定下来比后期返工划算得多。3. 拉通最小工程初始化代码、内存配置与 LCDConf 参数BSP 框架清楚了下面拉一个能跑的最小工程。整个最小工程就三个关键点main 里 board_init 和 GUI_Init 的顺序给 emWin 的内存池让 emWin 认识屏幕的 LCDConf。任何一个不对要么卡死要么显示错乱。下面按从上到下的路径一步步来。3.1 main 函数里的三板斧先外设再 GUI最后死循环#include GUI.h #include bsp.h void main(void) { board_init(); /* 第2章的初始化函数包含时钟/DDR/LCD/触摸 */ GUI_Init(); /* emWin初始化读取LCDConf、挂载显示驱动 */ GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetFont(GUI_Font16B_ASCII); GUI_DispStringAt(NonOS emWin OK, 100, 100); while (1) { GUI_Delay(10); /* 10ms心跳驱动窗口消息与动画 */ } }board_init 执行完之后 DDR 已经可用GUI_Init 里分配内存才安全。GUI_Delay(10) 在 NonOS 下不只是简单等 10ms它内部会调用 GUI_X_Delay让窗口管理器有时间处理消息、刷新无效区域、推进动画。如果不需要动态效果可以把延时改大降低功耗但触摸响应会变迟钝10ms 是手感比较折中的值。GUI_SetFont 里的字体资源必须已经链接进工程否则字符显示空白或乱码工程初期用自带字体排错最方便字体文件越少越容易定位。3.2 给 emWin 一块不会被抢走的内存GUI_X_Config 的静态数组写法#define GUI_NUMBYTES (128 * 1024) static U32 aMemory[GUI_NUMBYTES / 4]; void GUI_X_Config(void) { GUI_ALLOC_AssignMemory(aMemory, GUI_NUMBYTES); /* 把静态数组交给emWin */ GUI_ALLOC_SetMemHandle(0); GUI_SetDefaultFont(GUI_Font6x13); }GUI_X_Config 会被 GUI_Init 自动调用不要在 main 里重复调用。这段代码的作用是给 emWin 的内存管理器建立固定大小的内存池窗口对象、控件、字体缓存、存储设备都从池里分配。为什么 NonOS 下不用 malloc两个原因一是 BSP 里的外设 DMA 和驱动堆往往共用一个堆地址被覆写很难定位二是裸机堆缺乏保护长期运行碎片化严重。静态数组没有堆管理开销地址固定调试时一眼能看出越界区域。参数上GUI_NUMBYTES 先给 128KB运行一段时间后用 GUI_ALLOC_GetNumFreeBytes 打印剩余内存再往下调整。如果要用外部 DDR 做大内存池可以把 aMemory 改到外部 SDRAM 段但必须确认这块区域和帧缓冲 LCD_BASE、栈、BSS 段完全不重叠。我见过一个工程因为内存池覆盖了帧缓冲设备运行二十分钟后屏幕开始出噪点排查方向完全被带偏。3.3 LCDConf 三个必调参数尺寸、色深、帧缓冲地址/* LCDConf.h 典型片段 */ #define LCD_XSIZE 800 #define LCD_YSIZE 480 #define LCD_BITSPERPIXEL 16 #define LCD_SWAP_RB 1 /* 部分面板必须交换R/B否则颜色不对 */ #define LCD_USE_DMA 1 /* 用LCD DMA搬移像素块 */ #define LCD_BASE 0x81000000 /* DDR内的帧缓冲地址避开内存池 */这些宏决定 emWin 底层驱动往哪里画、画多大、用什么颜色编码。LCD_XSIZE 和 LCD_YSIZE 必须与面板物理分辨率一致如果面板实际安装方向是竖屏而逻辑上要做横屏通常是在 lcd_drv.c 里做旋转不要直接改这两个值否则坐标系统会乱。LCD_SWAP_RB 看起来小但错一个颜色通道整个 UI 偏色属于改配置工作量极小、排查却极其费时间的选项。LCD_BASE 要和链接脚本里的 DDR 布局对上。我在 N9H30 工程里习惯先把内存使用表列出来栈、堆、BSS、数据段、emWin 内存池、帧缓冲地址全部写清楚再找一块没有人用的区域给显存。很多工程的帧缓冲放在 DDR 偏移 2MB 之后避开启动代码和 BSS。LCD_USE_DMA 开启时emWin 的位块传输会走 DMA刷屏速度快很多但 DMA 初始化必须在 GUI_Init 之前完成如果发现刷屏偶尔残留直线优先排查 DMA 是否在总线繁忙时被抢占再看帧缓冲地址对齐 16 字节是否满足。工程第一次点亮我建议只做冒烟测试画全屏纯红、纯绿、纯蓝然后画对角线。颜色不对查 LCD_SWAP_RB图像错位或撕裂查像素时钟和 LCD_XSIZE/YSIZE。这一步过了再开始做界面。4. 给 emWin 一副裸机“心跳”节拍、触摸与主循环事件模型没有 RTOSemWin 不会自动获得时间。窗口动画、控件闪烁、GUI_Delay 全靠一个硬件定时器提供节拍触摸坐标要主动塞给 GUI 内部不能指望中断里直接操作控件。这一章是最容易被低估的——很多人移植完显示就以为大功告成结果动画全部静止才回头补这块。4.1 用 SysTick 造一个毫秒节拍GUI_X_GetTime 和 GUI_X_Delaystatic volatile U32 s_uiTickMs; /* 中断和主循环共享 */ void SysTick_Handler(void) { s_uiTickMs; /* 每1ms加一次 */ } U32 GUI_X_GetTime(void) { return s_uiTickMs; } void GUI_X_Delay(int Period) { U32 uiEnd s_uiTickMs (U32)Period; while (s_uiTickMs uiEnd) { /* 忙等等够毫秒数 */ } }SysTick 的中断周期需要配置成 1ms主频 300MHz 时重装载值就是300MHz / 1000- 1。GUI_X_GetTime 给 emWin 内部计时GUI_X_Delay 用于 GUI_Delay 和窗口管理器等待两者必须基于同一个计数器不能用两个不同的定时器否则时间基准对不上。参数上s_uiTickMs 一定要加 volatile否则编译器可能把主循环里的读取优化成常量界面就永远卡在初始状态。NonOS 下这些函数没有“让出 CPU”的能力只能忙等。如果工程里开了看门狗且喂狗逻辑在某条中断里长延时可能触发复位——这是裸机工程和 RTOS 工程的明显差异RTOS 里可以阻塞任务裸机里延时本质上是占满 CPU。我一般把看门狗喂狗放在主循环开头GUI_Delay 周期短问题不大如果延时长达数百毫秒就要重新考虑喂狗位置。4.2 触摸坐标如何进 GUI轮询、校准和抖动滤波static int map_touch(int raw, int rawMin, int rawMax, int lcdSize) { if (raw rawMin) raw rawMin; if (raw rawMax) raw rawMax; return (raw - rawMin) * lcdSize / (rawMax - rawMin); } void touch_task(void) { if (touch_is_pressed()) { int rawX touch_read_raw_x(); int rawY touch_read_raw_y(); int x map_touch(rawX, ADC_X_MIN, ADC_X_MAX, LCD_XSIZE); int y map_touch(rawY, ADC_Y_MIN, ADC_Y_MAX, LCD_YSIZE); GUI_TOUCH_StoreState(x, y, 1); /* 按下 */ } else { GUI_TOUCH_StoreState(-1, -1, 0); /* 抬起 */ } }N9H30 的 BSP 要么用芯片内置 ADC 采电阻屏要么走 I2C/SPI 读电容触摸控制器不管哪种都要把原始坐标统一换算成 LCD 像素坐标再交给 GUI_TOUCH_StoreState。GUI_TOUCH_StoreState 是 emWin 的触摸事件入口窗口管理器靠它判断点击、移动和释放。参数上如果面板做了旋转这组 linear 映射里的 X/Y 不一定直接对应 LCD 坐标系常见做法是加一个方向枚举再做旋转和镜像。ADC 的原始范围不能直接照抄手册理论值每一批触屏都有偏差。我会在设备上电后做一次两端采样把最小值和最大值写进配置结构体运行起来再按这个范围映射。为了抑制电阻屏抖动采 5 次取中值再参与换算实际体验比单次采样稳很多。触摸校准还能再往深做比如三点校准矩阵但工程初期线性映射足够暴露大部分问题。4.3 主循环怎么把按键中断和 GUI 事件接起来volatile int g_iKeyEvent 0; void KEY_IRQHandler(void) { g_iKeyEvent 1; /* 中断里只置标志 */ } void main(void) { board_init(); GUI_Init(); while (1) { if (g_iKeyEvent) { g_iKeyEvent 0; /* 用窗口消息发给当前聚焦窗口或直接改控件值 */ WM_SendMessageNoPara(WM_GetClientWindow(hDialog), WM_KEY, 0); } touch_task(); GUI_Delay(10); } }中断里不要调用 GUI 函数emWin 本身不是可重入的裸机下也没有互斥锁保护。按键中断只管置标志主循环查一次、清一次再向窗口发消息。触摸也放在主循环里轮询与 GUI_Delay 交替执行。参数上GUI_Delay(10) 把主循环周期基本钉在 10ms触摸响应最多延迟一个周期操作手感不会有问题。如果这个循环里还要跑通信协议注意协议处理不能有长阻塞先把数据收进缓冲下一轮再解析避免界面掉帧。5. N9H30 裸机 emWin 避坑指南五个翻车现场裸机 emWin 出问题时十有八九不是 emWin 本身而是 BSP 里某个初始化参数或内存地址在作怪。下面五个现象都在 N9H30 工程里见过直接按“现象、原因、解法”写出来路上遇到了可以对照。5.1 上电花屏或错位条纹现象是开机后花屏、色条或者一半图像被切走但 GUI_Init 没有卡死系统还在跑。原因通常是 LCD 像素时钟比面板规格高太多或者 HSYNC/VSYNC 极性配置反了也可能是 DDR 稳定等待时间不够帧缓冲读出来有坏点表现为同一位置反复出噪点。解法是先按面板数据手册重新算像素时钟和时序参数再把 DDR 初始化里的等待循环加长到几十毫秒最后检查 HSYNC/VSYNC 极性和前肩后肩参数。这些配置都在 lcd_drv.c 里跟 emWin 没有关系。5.2 GUI_Init 卡死或开机 HardFault现象是仿真器停在 GUI_Init 内部或者一运行就进 HardFault。原因大多是给 emWin 的内存池和帧缓冲、BSS 段地址重叠或者栈被 GUI_Init 的局部变量撑爆。解法是把工程的内存使用表画出来确认 aMemory、LCD_BASE、栈和堆四块区域没有交集。经验做法是栈开到 8KB 以上GUI_NUMBYTES 按需设置而不是越大越好——内存池太大反而可能越界到帧缓冲。如果 HardFault 发生在 GUI_X_Config 之前先检查启动文件里的堆栈指针是否指向有效的 DDR 区域DDR 没起来的工程第一行 C 代码就可能翻车。5.3 触摸校准后仍旧不准点按钮总是偏现象是点一个按钮总是偏下或偏右重新校准之后仍有固定偏差。原因是 LCD 扫描方向和触摸坐标方向没有同步旋转或者 ADC 采样没有滤波单次值偏差太大。解法是把触摸映射函数加旋转分支比如横屏安装时把 rawY 取反或交换 X/Y并做 5 次采样中值滤波。我踩过一次很深的坑屏幕旋转了 180 度触摸只做了 X 取反Y 没取反结果对角位置怎么点都打不到改成方向枚举之后一次治好。这个问题最迷惑的地方在于中间区域看着准边界偏差被放大。5.4 GUI_Delay 不工作动态控件全部静止现象是界面显示正常但进度条、动画、滑动效果全部不动像死机。原因是 SysTick 中断没开启或者中断向量表不指向自定义的 SysTick_Handler还有一种是 GUI_X_GetTime 里忘了写 return被编译器优化成固定值。解法是先查 SysTick 控制寄存器使能位再在调试器里看 s_uiTickMs 是否每秒增长 1000 次如果变量不变查启动文件里的向量表是否被覆盖。GUI_X_GetTime 和 GUI_X_Delay 必须共用同一个计数器不能用别的延时函数凑合否则窗口管理器的超时判断会错乱。5.5 刷新大面积闪烁像屏幕在呼吸现象是窗口切换或 Slider 拖动时整块屏幕先空白一下再出图。原因是没有开启窗口的内存设备每次重绘直接打到 LCD数据量大就会出现可见闪烁如果底层还是软件写点驱动更加明显。解法是在 GUI_Init 后加一句 WM_SetCreateFlags(WM_CF_MEMDEV)让窗口先画在内存再整体刷新复杂背景用 GUI_MEMDEV_Create 构建存储设备。这个方法会多消耗内存内存吃紧时退而求其次用 WM_SetCallback 做局部重绘只刷新变化矩形。判断是不是这个问题很简单把窗口模式改成内存设备立竿见影如果内存不够再考虑局部重绘。遇到白屏这类整体性问题先别急着怀疑 emWin按“时钟、DDR、LCD时序、显示驱动、内存地址、GUI配置”的顺序排查比在 GUI_Init 里逐行跟进去更高效。6. 用内存设备和局部重绘把帧率再拉高一档一个必学技巧跑通界面只是第一步N9H30 做 HMI 项目真正烦的是帧率。很多人以为帧率瓶颈在 CPU 主频实际在软硬件的刷新方式。拿一个静态仪表盘界面举例背景圈、刻度、指针每次刷新都全屏重绘明显掉帧。我一般把静态部分构建在内存设备里运行时只把变化区域拷贝到 LCD帧率能翻倍。void create_ui_gui_memdev(void) { hMemPanel GUI_MEMDEV_Create(0, 0, 320, 240, GUI_MEMDEV_HASTRANS, 0); GUI_MEMDEV_Select(hMemPanel); /* 绘制不常变化的背景边框、刻度、静态文字 */ GUI_SetBkColor(GUI_BLACK); GUI_Clear(); GUI_SetPenSize(2); GUI_DrawArc(160, 120, 100, 0, 360); GUI_MEMDEV_Select(0); /* 切回LCD显示上下文 */ } void refresh_ui(void) { /* 每次只画指针位置等动态元素最后整体拷过去 */ GUI_MEMDEV_Select(hMemPanel); GUI_SetColor(GUI_BLACK); GUI_FillCircle(160, 120, 80); /* 清除旧指针区域 */ GUI_SetColor(GUI_RED); GUI_DrawLine(160, 120, 160 newX, 120 - newY); /* 画新指针 */ GUI_MEMDEV_Select(0); GUI_MEMDEV_CopyToLCD(hMemPanel); }GUI_MEMDEV_Create 里的窗口尺寸越小内存占用越小速度越快GUI_MEMDEV_HASTRANS 是透明混合标志背景不需要透明时可以去掉能省一段内存。GUI_MEMDEV_CopyToLCD 是整块搬运比软件逐点画快一个数量级。如果项目用了 WM 管理界面再用 WM_InvalidateRect 只标记变化区域比如数字刷新时只重绘一个小矩形而不是调用 WM_InvalidateWindow 刷整窗——这就是典型的“差一行代码帧率差一半”。这个技巧背后还有一个小教训存储设备如果在运行期频繁 Create 和 Delete动态内存会碎片化设备跑几天后可能出现内存不足。我习惯在初始化阶段建好所有内存设备运行期只用 Select 和 Copy不反复创建。第一次做 N9H30 裸机工程时我直接把整个界面丢给全量重绘结果又闪又慢改成内存设备加局部重绘之后才达到可用的帧率。如果你也遇到界面卡顿先看一眼是不是全窗重绘在作怪再决定要不要上这层优化。希望帮到你。本文还有配套的精品资源点击获取