STM32F103+RA8875+emWin+uCOSIII 嵌入式GUI显示方案完整解析

STM32F103+RA8875+emWin+uCOSIII 嵌入式GUI显示方案完整解析 简介基于STM32F103单片机的完整工程源码整合RA8875图形控制器、emWin526图形库、uCOSIII实时操作系统与液晶板载字库面向需要构建带图形界面嵌入式显示方案的中高级开发者也适合学习RTOS与GUI集成的学生参考。压缩包约4.3MB共291个文件以162个C语言头文件、90个C源文件为主体另含批量处理脚本、汇编启动文件、编译链接工具、库文件、Keil工程配置文件和PDF说明文档目录结构清晰便于按功能模块定位代码。已有199人学习下载。源码覆盖STM32F103与RA8875的底层驱动、emWin526的移植和配置以及uCOSIII的任务创建、同步与事件调度同时演示了如何使用板载字库在液晶屏上高效显示中文与图形。通过阅读工程可快速掌握LCD图形界面与实时操作系统协同开发的方法理解如何在资源受限的MCU上实现流畅的2D图形操作和文字渲染完整可编译的Keil工程也便于直接调试与二次开发为基于RA8875的智能仪表、人机交互面板等产品提供可复用的移植基础。1. 基于STM32F103与RA8875-emWin5.26-uCOSIII的显示方案在讲什么一个 STM32F103 单片机同时面对 RA8875 图形控制器、emWin5.26 图形库和 uCOSIII 实时系统这条技术链路在仪器仪表和工控面板上已经大量量产。RA8875 自带显存和 GB2312 点阵字库把 CPU 从“取字模、拼像素”里解放出来emWin5.26 负责窗口、控件和触控逻辑uCOSIII 把触摸扫描、界面刷新、业务逻辑切成独立任务。标题里的“LED(液晶板载字库)”点明这块屏不是裸屏而是把背光控制和字库都做进了控制器省掉了外扩 Flash。适合三类人看手头有 F103 和 RA8875 屏幕正在做选型的系统跑着裸机想上 GUI 的以及拿到这类源代码包想知道哪些是核心文件、哪些可以放心删掉的人。2. STM32F103 用 FSMC 接 RA8875 的接线与寄存器初始化要点2.1 8080 并口时序与 F103 的 FSMC 引脚映射RA8875 的显示数据接口有 8080、6800 两种并口和 SPI 串口。SPI 虽然省引脚但 800x480 全屏刷新会卡到不可用6800 时序在 51 单片机的老设计里常见F103 上最快的还是 8080 并口。把 RA8875 当一块 NOR FLASH 挂在 FSMC Bank1 上STM32 的 FSMC 硬件会自己产生读写时序CPU 写一次地址就是一次 FSMC 写周期不需要 GPIO 模拟时钟线。STM32F103 引脚FSMC 功能接 RA8875说明PD0~PD15FSMC_D0~D15DB0~DB1516 位数据线用 8 位模式时可只用 D0~D7PD4FSMC_NOERD#读使能PD5FSMC_NWEWR#写使能PD7FSMC_NE1CS#片选对应 Bank1 区PE1 或任选 IO普通 IORS命令/数据选择FSMC 不直接参与RS 引脚是很多人第一次接线会卡住的地方。FSMC 的地址线可以映射 RS比如把 RS 接 FSMC_A0那么地址 0x60000000 是命令口0x60000002 是数据口我一般习惯把 RS 单独接在普通 IO 上操作前先拉高拉低逻辑直观出问题时也好用万用表量。FSMC 初始化代码里需要重点调两个时序参数FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 1; /* 地址建立时间单位 HCLK */ timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 3; /* 数据建立时间决定写脉冲宽度 */ timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_AccessMode FSMC_AccessMode_B;逻辑说明RA8875 手册要求写周期典型值 50nsF103 在 72MHz 下 1 个 HCLK 约 13.9nsDataSetupTime 取 3 就是约 42ns能压线工作。若屏幕出现写不进、颜色错乱、花屏第一个动作就是把 DataSetupTime 从 3 改到 7牺牲一点速度换稳定。参数说明FSMC_AccessMode_B 是按 NOR Flash 的异步模式读写的常用选择若你的板子用了 8 位数据宽度FSMC_MemoryDataWidth 要改成 8bitRA8875 那边也要把面板接口跳线换成 8 位两边不一致时高层画面会整体错位。2.2 RA8875 上电初始化的寄存器顺序为什么不能乱RA8875 上电后默认是显示关闭的。初始化顺序错了的后果很有意思如果先开背光和显示再配 PLL可能出现一瞬间的白屏或花屏如果显存窗口参数配在 PLL 之前某些批次芯片会直接不响应写显存操作。标准顺序是硬件复位保持低电平 10ms 以上拉高后再等 20ms配置 PLL1、PLL2 分频等 5~10ms 锁定配置水平/垂直扫描周期、显示窗口和面板分辨率一一对应配置显存起始地址、色彩深度最后配置电源控制寄存器打开显示再决定是否开背光void ra8875_init(void) { ra8875_hw_reset(); /* RESET 拉低 10ms 再拉高 */ delay_ms(20); ra8875_write_reg(RA8875_PLL1, 0x0C); /* 系统时钟倍频 */ ra8875_write_reg(RA8875_PLL2, 0x02); /* 内存时钟分频 */ delay_ms(10); /* 等 PLL 锁定期间不写显存 */ ra8875_write_reg(RA8875_PWRR, 0x00); /* 先关显示确保后续写入安全 */ ra8875_write_reg(RA8875_HSCWR, 0x80); /* 水平扫描循环宽度 */ ra8875_write_reg(RA8875_HSSTR, 0x03); /* 水平扫描起点 */ ra8875_write_reg(RA8875_HDWTR, 0x1F); /* 水平显示宽度 */ ra8875_write_reg(RA8875_VDWTR, 0xDF); /* 垂直显示高度 */ ra8875_write_reg(RA8875_HSAWR, 0x00); /* 显存水平起始地址 */ ra8875_write_reg(RA8875_DPCR, 0x02); /* 16 位色深 RGB565 */ ra8875_write_reg(RA8875_PWRR, 0x03); /* 打开显示 */ }逻辑说明RA8875_PLL1 到 RA8875_PWRR 这些宏要在头文件里按芯片手册的寄存器索引定义不同批次的手册存在细微差异务必以模组厂商给的最新手册为准。PLL 锁定前写显存会丢数据所以两个 delay_ms 不能省。DPCR 的 0x02 对应 16 位色若面板是 8 位并口要改成 0x00颜色量纲也会不同后续所有颜色值都要跟着调。参数说明HSCWR、HDWTR、VDWTR 的数值来自液晶面板扫描规格800x480 屏和 480x272 屏这些值完全不一样不能照抄其他工程。最稳妥的办法是找屏幕模组厂商要 RA8875 配置表通常是一份 Excel 或 PDF照着填即可。2.3 初始化失败从现象反推原因屏幕全白但背光亮说明 PLL 没锁定就开始写显示寄存器把等待时间加到 30ms 再试。花屏且颜色发紫先查色深配置RGB565 当 RGB666 发就会出现这种问题。只能在左上角显示一小块显示窗口参数和面板分辨率不匹配或 HSAWR 高位校验没做。全黑且背光不亮PWRR 的 DISP 位没置起来背光引脚也没给 PWM。这些现象排查顺序比逐个寄存器扫描快得多我在现场调屏时基本都从这三类问题入手。3. emWin5.26 移植把 RA8875 挂成显示设备的三个关键文件3.1 先想清楚用模板驱动还是直接写硬件emWin 5.26 的底层驱动分两类。一类是模拟 LCD 驱动emWin 把像素画到一块由你提供的缓冲区再一次性拷贝到显示器另一类是直接写硬件驱动每画一个像素就调你的 SetPixel 函数。RA8875 自带显存并且有硬件填充、BTE 块搬运直接写硬件是更常见的做法因为不需要为 800x480 分配 384000 字节的缓冲F103 内部 SRAM 最大 64KB根本不够。实际工程的配置一般是emWin 的存储设备只用来保存小图标和界面位图像素刷新全部走 RA8875 的 BTE。这样 GUI_MALLOC_SIZE 不用很大窗口多了也不怕内存碎片。3.2 GUIConf 里需要重点调的 4 个配置项配置项建议值原因GUI_NUMBUFFERS1RA8875 自带显存emWin 侧不需要双缓冲GUI_BUFFERSIZE4x1024只给 GUI_Exec 用临时缓冲够用即可GUI_MALLOC_SIZE10~30KB控件和位图都从这分F103 总共 64KB别贪大GUI_DEFAULT_FONTGUI_Font8x168x16 点阵在 3.5 寸屏上可读性最好这组参数在 GUIConf.c 里不在 LCDConf.c。改了 GUI_MALLOC_SIZE 后如果一创建窗口就 HardFault多半是堆大小超过了内部 SRAM 剩余空间正确做法是减 uCOSIII 任务栈而不是硬加内存你总得给 GUI 任务留出栈空间。3.3 LCD_X_DisplayDriver 的实现骨架emWin 通过 LCD_X_DisplayDriver 回调把绘图请求分发给底层驱动这是移植最核心的一个函数int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void *pData) { int r 0; switch (Cmd) { case LCD_X_INITCONTROLLER: ra8875_init(); r 1; break; case LCD_X_SETPIXELINDEX: { LCD_PIXELINDEX_OFFSET *p (LCD_PIXELINDEX_OFFSET *)pData; ra8875_set_pixel(p-x, p-y, p-PixelIndex); r 1; break; } case LCD_X_FILLRECT: { LCD_RECT *pr (LCD_RECT *)pData; ra8875_fill_rect(pr-x0, pr-y0, pr-x1 - pr-x0 1, pr-y1 - pr-y0 1, LCD_COLORINDEX); r 1; break; } default: break; } return r; }逻辑说明SetPixel 是 emWin 的最基础原子操作所有绘制最终都退化成它。FILLRECT 是关键分支如果不实现一个带背景色的大窗口会被 emWin 一行行调 SetPixel而 RA8875 的填充命令可以一条指令刷一个矩形性能差距在 10 倍以上。参数说明LayerIndex 在多图层时用来区分图层RA8875 支持两层显示但大多数工程只用 0 层直接忽略。LCD_COLORINDEX 要和 RA8875 的颜色格式保持一致工程里一般把 LCD_CONF 配成 RGB565这样颜色值可以直接写显存不用做颜色空间转换。3.4 BTE 加速重绘的一个常见坑BTE 是 RA8875 把内存块里面素搬移、填充动作交给硬件的机制。我的建议是大区域重绘用 BTE小图标和文字继续走 SetPixel不要看到 BTE 就全部换。原因是 BTE 每次发起要写几个寄存器开销几十微秒绘制几个像素的图标反而不划算。另外 BTE 操作是异步的如果 CPU 不等待完成标志紧接着写显存会覆盖正在搬运的数据屏幕出现半幅残影。正确做法是发起 BTE 后轮询状态寄存器等 done 位或者保证后续至少间隔一个 BTE 周期。4. uCOSIII 下的任务划分与 GUI 资源保护4.1 任务优先级与栈分配的思路uCOSIII 里 GUI 任务优先级比触摸任务高比硬件中断低。关键是要给 GUI 任务留足栈我见过太多人照抄评估板任务表GUI 任务栈只给 512 字结果窗口一多就 HardFault。任务名优先级栈大小说明start_task2512x4创建各任务后自删除gui_task51024x4emWin 主任务4KB 栈跑 3 个窗口有余量touch_task6256x4触摸扫描20ms 周期led_backlight8128x4背光 PWM 渐变任务触摸任务读到的坐标放在共享结构体绝不直接调 GUI 函数led 任务如果只做背光呼吸可以不做 PWM 中断用 OSTimeDly 按 10ms 步进改占空比寄存器即可。4.2 GUI 任务独占 emWin其他任务发消息emWin 不是线程安全的。多个任务同时调用 GUI_XXX 会导致窗口句柄混乱、控件状态错乱有时低优先级任务画到一半被抢占刷屏刷出残影。常见做法是两条路第一所有 GUI API 单独放进 gui_task其他任务想改界面只发消息队列第二在 gui_task 里用互斥信号量包住整段绘制逻辑。void gui_task(void *p_arg) { OS_ERR err; GUI_Init(); create_main_window(); for (;;) { OSMutexPend(gui_mutex, 0, OS_OPT_PEND_BLOCKING, NULL, err); GUI_Exec(); /* 执行定时器、窗口回调 */ OSMutexPost(gui_mutex, OS_OPT_POST_NONE, err); OSQPost(gui_msg_q, msg, 0, OS_OPT_POST_FIFO, err); OSTimeDlyHMSM(0, 0, 0, 10, OS_OPT_TIME_HMSM_STRICT, err); } }逻辑说明互斥锁放在 GUI_Exec 外面而不是里面是因为 GUI_Exec 会连续执行一批定时器和回调如果锁嵌套太深等待时间过长F103 在 72MHz 下会明显感到界面卡顿。OSQPost 是给其他任务一个“界面已处理完”的信号触摸任务可以根据这个信号决定是否继续发坐标。还有一处必须改emWin 的 GUI_X_OS.c 里默认的锁接口要在 uCOSIII 下换成 OSMutexPend 实现否则 GUI 内部的临界区起不到保护作用。GUI_X_OS_LOCK 函数里 Pend 一个专用互斥量GUI_X_OS_UNLOCK 里 Post 它。这个接口不换前面所有加锁代码都白搭。4.3 触摸任务与 GUI 任务的时间片配合触摸任务执行周期设 20ms也就是 50Hz 采样对电阻屏已经够用。它读取 x/y/按下状态后写入共享结构体置一个脏标志。gui_task 每 10ms 检查标志置了才调用 WM_TouchUpdate 或 GUI_TOUCH_StoreState。不要让触摸任务直接调 GUI_TOUCH_StoreState如果恰好在重绘中途插入控件会收到断开的触摸序列。void touch_task(void *p_arg) { OS_ERR err; while (1) { touch_read(touch_state); /* 读触摸控制器 */ touch_dirty 1; /* 置脏标志由 gui_task 消费 */ OSTimeDlyHMSM(0, 0, 0, 20, OS_OPT_TIME_HMSM_STRICT, err); } }参数说明20ms 对应 50Hz再低就会出现滑动不跟手电容屏可缩短到 8ms但 F103 的 I2C 读取本身要时间太快反而占总线。脏标志要被两个任务访问定义成 volatile保护临界区用 uCOSIII 的 OS_CRITICAL_ENTER 即可不必为此加重型锁。4.4 一个容易被忽略的 HardFault 来源GUI 任务栈溢出时uCOSIII 的钩子函数能帮你拦截并强制挂起但栈溢出发生在 emWin 内部时可能直接 HardFault钩子根本来不及执行。排查方法开启 OS_CFG_TASK_STK_OVF_CHECK在 gui_task 里周期性调用 OSTaskStkChk 查看已用水位。如果发现栈增长到 90% 以上不要只加栈大小先减少 GUI_MALLOC_SIZE 或缩小窗口数量因为栈和堆在 64KB SRAM 里是此消彼长的。我还遇到过一个隐蔽问题STM32F103 最小系统板上FSMC 和 JTAG 引脚复用了 PD 口烧录器一直连接不上。排查到最后是 BOOT0 没拉低烧录模式搞错了。这类基础问题在调试 GUI 之前务必先确认别等代码写完才发现下载不进去。5. 板载字库与 emWin 字体共存的两个实用场景RA8875 的板载字库意味着不用给中文配字库文件也不用拷点阵进外部 Flash。显示中文时切到文本模式写内码控制器自己查点阵。LED 背光控制也集成在 RA8875 里寄存器直接调 PWM 占空比不需要外接背光驱动芯片。5.1 用硬件字库显示中文标签void gui_show_hw_text(int x, int y, const char *s, uint16_t fg, uint16_t bg) { ra8875_enter_text_mode(); /* 切到字库文本模式 */ ra8875_set_cursor_pos(x, y); ra8875_set_color(fg, bg); while (*s) { ra8875_write_byte(*s); /* 连续写内码控制器自动查字模 */ } ra8875_exit_text_mode(); /* 切回图形模式 */ }逻辑说明这里不需要逐像素操作也不需要查字模数组RA8875 收到字符内码后从内部 ROM 取点阵并按颜色写到当前坐标CPU 只搬运字节。一屏 20 个汉字的数据量不超过 40 字节。参数说明x、y 是像素坐标但文本模式按字符网格对齐8x16 点阵时 y 要落在 16 的倍数上否则字符之间出现半行错位。fg 和 bg 在 RA8875 内部按 RGB565 存emWin 传过来的也是 RGB565同一个颜色宏可以两边通用。5.2 混排时的刷新顺序与验证方法混排时先让 emWin 画背景和数值区再切文本模式写中文标签顺序反了会把 emWin 的窗口内容盖掉。标签位置如果被控件遮挡要先填充矩形再写字文本模式的痕迹不会自动消失。验证刷新性能最直接的办法在 gui_task 里包一段 GPIO 翻转示波器量高电平宽度。GUI_Exec 前拉高执行完拉低统计一次完整界面刷新时间。800x480 下整屏背景重绘应低于 80ms局部区域刷新应低于 20ms。如果差距大回去检查 FILLRECT 分支是否走了 SetPixel 逐点路径。这套验证方法不依赖任何上位机工具现场调屏时最实用。本文还有配套的精品资源点击获取