
简介基于STM32F103单片机与RA8875液晶控制器结合emWin526图形库和uCOS-III实时操作系统构建了一套带中文显示的嵌入式图形界面工程适合需要学习GUI移植、RTOS多任务调度和液晶驱动开发的工程师。资源共291个文件以162个h头文件和90个c源文件为主另有bat批处理、汇编启动文件、exe工具及PDF说明文档压缩包约4.3MB目录按驱动、系统、界面模块划分便于分块查阅。已有199人学习下载。源码中包含STM32F103与RA8875的底层驱动emWin的完整移植与配置uCOS-III的任务管理、事件调度与内存管理以及液晶板载字库显示样例RA8875内置图形加速器可简化2D绘图emWin提供窗口、控件等界面元素板载字库则降低了运行时内存占用。批处理脚本覆盖80×60、320×240等多种分辨率生成流程可直接用于快速构建工业人机交互或教学实验项目也可作为相关课程设计或产品原型的基础参考。1. 把 RA8875、emWin526、uCOS-III 叠在 STM32F103 上图什么只点亮一块屏GPIO 模拟时序就够但要做到 800x480 彩色界面、动态曲线、中文字库、多串口通信再加 AD 采集裸机状态就是给自己挖坑。这套组合的真实分工是RA8875 扛下清屏、填充、BTE 位块搬运和 GT 字体绘制STM32F103 的 Cortex-M3 内核从像素操作里解脱出来专心跑 uCOS-III 的任务调度emWin526 在中间层处理窗口、控件、WM 消息应用层只需要写业务逻辑。适合工控面板、仪器仪表、带触摸的 HMI 这类项目也适合想搞懂「GUI 库 RTOS LCD 控制器」三层软件怎么咬合的人。这份源码值得拆的不是功能效果而是接口、任务边界和初始化时序的取舍。2. RA8875 驱动移植FSMC 并行时序与板载字库初始化链路2.1 为什么让 RA8875 挂在 FSMC 上而不是 SPIRA8875 本身提供 8080 并口总线也支持串行 SPI 从模式。STN/TFT 屏刷新需要持续搬运像素SPI 在 36MHz 下理论吞吐约 4.5MB/s实际传输要打折FSMC 把并行数据传输变成一次普通内存读写的开销代码上就是往地址写值编译器帮你处理时序。STM32F103 里只有大容量256KB Flash 以上型号带 FSMC 控制器这是选型时先要确认的点。我一般把 RA8875 映射到 BANK1 的 NOR/SRAM 区NE3 片选A16 地址线接 RA8875 的 RS 引脚控制芯片处于「命令寄存器地址」还是「数据寄存器地址」。标准外设库 V3.5.0 的 FSMC 例程可以直接改关键配置如下static void RA8875_FSMC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef FSMC_TimingInitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); /* DB0~DB7 接 PD0~PD7, RD 接 PD4, WR 接 PD5 */ /* A16 接 RS 引脚, NE3 接 CS 引脚 */ FSMC_TimingInitStructure.FSMC_AddressSetupTime 1; FSMC_TimingInitStructure.FSMC_AddressHoldTime 0; FSMC_TimingInitStructure.FSMC_DataSetupTime 6; FSMC_TimingInitStructure.FSMC_BusTurnAroundDuration 0; FSMC_TimingInitStructure.FSMC_CLKDivision 0; FSMC_TimingInitStructure.FSMC_DataLatency 0; FSMC_TimingInitStructure.FSMC_AccessMode FSMC_AccessMode_B; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAM3; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_8b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_WaitSignal FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct FSMC_TimingInitStructure; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct FSMC_TimingInitStructure; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE); }这段代码里FSMC_AddressSetupTime 1是地址建立时间FSMC_DataSetupTime 6是数据建立时间。RA8875 的读写时序典型在几十纳秒级STM32F103 跑 72MHz 时这两个参数一般取 1~10 都能工作但如果屏幕出现雪花或者偶尔花屏优先加大FSMC_DataSetupTime不要一上来就怀疑字库坏了。数据宽度用 8bit 就够RA8875 像素数据是按字节访问的没有必要配置成 16bit。接着定义访问宏#define RA8875_REG (*(volatile uint8_t *)0x60020000) /* A16 1命令寄存器 */ #define RA8875_RAM (*(volatile uint8_t *)0x60000000) /* A16 0数据寄存器 */基址 0x60000000 对应 NE3 片选A16 为 1 时地址加了 (1 16)。宏定义之后写寄存器、读状态、写像素数据都是普通赋值编译器和 FSMC 时序保证信号正确这比用 GPIO 翻转快两个数量级。2.2 上电复位与寄存器初始化链RA8875 上电后 PWRR电源控制寄存器地址 0x01默认处于复位状态直接写显示参数不生效。正确的初始化顺序是硬件复位拉低 RESET 引脚至少 100us再拉高写 PWRR 关闭内部电路配置像素时钟和 H/V 时序最后写 PWRR 打开显示。顺序反了会出现「寄存器写进去了但不显示」的假故障。我整理过的初始化函数骨架static void RA8875_Init(uint16_t width, uint16_t height) { /* 硬件复位拉低 1ms再拉高等待内部 PLL 稳定 */ RA8875_RESET_LOW(); DelayMs(1); RA8875_RESET_HIGH(); DelayMs(50); RA8875_WriteReg(0x01, 0x80); /* PWRR关闭显示进入配置模式 */ DelayMs(10); /* PCSR 0x04像素时钟选择按屏规格计算 */ /* 对 320x240 的 5 寸屏常见配置为分频后约 10~20MHz */ RA8875_WriteReg(0x04, 0x01); /* 水平同步宽度、周期、显示宽度 */ RA8875_WriteReg(0x88, LCD_HSYNC_WIDTH - 1); RA8875_WriteReg(0x89, LCD_HSYNC_START - 1); RA8875_WriteReg(0x8A, LCD_HSYNC_PW - 1); /* 垂直同步宽度、周期、显示高度同理 */ RA8875_WriteReg(0x8B, LCD_VSYNC_HEIGHT - 1); RA8875_WriteReg(0x8C, LCD_VSYNC_START - 1); RA8875_WriteReg(0x8D, LCD_VSYNC_PW - 1); /* 设置显示窗口尺寸以像素为单位 */ RA8875_WriteReg(0x14, (width - 1) 8); RA8875_WriteReg(0x15, (width - 1) 0xFF); RA8875_WriteReg(0x16, (height - 1) 8); RA8875_WriteReg(0x17, (height - 1) 0xFF); /* 打开显示使能内部电路 */ RA8875_WriteReg(0x01, 0xC0); DelayMs(10); }注意HSYNC_START和HSYNC_PW这些参数不能从 RA8875 数据手册里直接抄一个数组过来用必须对照你手上那块屏的 datasheet 的时序图或者干脆用销售商提供的初始化代码。不同批次屏参数偏差不大但确实遇到过屏参不匹配导致画面整体偏移 10 个像素的情况症状是右边有一条黑边。这里补充一个常见误区有人把 STM32F103 的 FSMC 配置成了复用推挽但没有开启 AFIO 时钟和对应的 GPIO 重映射结果数据线完全不通。排查方法是在初始化后立刻回读 SYSR地址 0x10寄存器SYSR 的 bit7 是 PowerON 标志读不到 0x80 就说明 RESET 或 PWRR 那一步有问题不用继续往下查。2.3 板载字库GT 字体层与中文字符显示「板载字库」通常指 RA8875 外挂一颗 SPI NOR FlashMCU 通过 RA8875 间接访问或者 RA8875 内部 ROM 里直接带 ASCII 字模。本项目如果屏上自带了字库芯片MCU 侧就只需要往 RA8875 发字体选择命令和字符编码硬件会自动去 Flash 里取字模并绘制到当前光标位置省掉从 MCU 搬运点阵数据的 CPU 周期。RA8875 的文本模式支持多行多列字符写入中文字库一般按 GB2312/GBK 编码组织每个汉字 2 字节字模按区位码偏移。实际驱动代码如下void RA8875_PutText(uint16_t x, uint16_t y, const char *str, uint16_t fgColor, uint16_t bgColor) { RA8875_WriteReg(0x63, fgColor 8); /* 前景色高字节 */ RA8875_WriteReg(0x64, fgColor 0xFF); /* 前景色低字节 */ RA8875_WriteReg(0x65, (0x0F) 0x0F); /* 文本颜色不透明模式 */ while (*str) { RA8875_SetCursor(x, y); if (*str 0xA1) { /* 汉字先写高位字节再写低位字节 */ RA8875_WriteData(*str); RA8875_WriteData(*str); x 16; /* 16 点阵字宽 */ } else { RA8875_WriteData(*str); x 8; /* ASCII 半角 */ } } }参数上fgColor和bgColor是 16 位 RGB565 格式高位字节在前。这个函数有一点要注意RA8875_SetCursor每次循环都执行如果是连续字符串RA8875 内部会自动换行换列频繁设置光标反而可能丢掉字间距信息工程化做法是遇到\n时才重新设置行列。另外一个坑是 GB2312 汉字编码范围是 0xA1-0xFE内部 ROM 字库还区分「半角 ASCII」和「全角符号」符号区字宽可能是 16 也可能是 8跟具体字库版本有关我在移植时用了一张映射表把设备型号和字宽对应起来避免全角括号显示成两个残缺汉字。3. emWin526 与 uCOS-III 的任务协作机制3.1 让 emWin 在 RTOS 里跑起来的五个接口emWin526 是多线程安全的但前提是用户层把 OS 相关的接口补齐。它内部通过GUI_X_OS这组函数完成时间查询、延时、互斥锁操作不实现这组函数emWin 在 uCOS-III 上跑起来就是裸奔一旦有多个任务同时调GUI_DrawLine画面就会出现随机缺失的矩形块。裸机工程常见的改动是在GUIConf.h里打开这些开关#define GUI_OS 1 /* 使能 OS 相关接口 */ #define GUI_SUPPORT_TOUCH 1 /* 如果板上没触摸关掉可省 RAM */ #define GUI_DEFAULT_FONT GUI_FontSong16 /* 中文字体按需配置 */ #define GUI_NUMBYTES (30 * 1024) /* emWin 动态内存池大小 */ #define GUI_WINSUPPORT 1 /* 窗口管理器 */ #define GUI_SUPPORT_MEMDEV 1 /* 存储设备用于加速窗口重绘 */GUI_NUMBYTES非常关键。STM32F103 最大也只有 64KB SRAM如果窗口多、分辨率高3 个 32 位像素缓冲就把内存吃光了。我的经验是 320x240 无触摸界面至少要 20KB带曲线和中文输入法候选框要 40KB内存不够时GUI_Init会返回异常或者运行一段时间后死机。uCOS-III 任务栈也不能省GUI 任务栈通常要 4~8KB不能按裸机习惯给 1KB。实际实现GUI_X_uCOS.c时核心代码是这几个函数的映射OS_MUTEX GUI_Mutex; void GUI_X_InitOS(void) { OS_ERR err; OSMutexCreate(GUI_Mutex, emWin Lock, err); } void GUI_X_Lock(void) { OS_ERR err; OSMutexPend(GUI_Mutex, 0, OS_OPT_PEND_BLOCKING, NULL, err); } void GUI_X_Unlock(void) { OS_ERR err; OSMutexPost(GUI_Mutex, OS_OPT_POST_NONE, err); } U32 GUI_X_GetTime(void) { OS_ERR err; return (U32)OSTimeGet(err); } void GUI_X_Delay(int ms) { OS_ERR err; OSTimeDly(ms * 1000 / OS_CFG_TICK_RATE_HZ, OS_OPT_TIME_DLY, err); }这里有几个细节值得说明。GUI_X_Lock用的是等待时间无限期的互斥信号量0表示永久等待如果 GUI 锁被一个被挂起的任务持有另一个任务就会一直等下去所以 GUI 任务优先级不要设太低否则低优先级任务抢到锁后又被高优先级任务打断造成锁长时间不释放。GUI_X_Delay换算时emWin 的入参单位是毫秒uCOS-III 的OSTimeDly以 tick 为单位OS_CFG_TICK_RATE_HZ在os_cfg_app.h里定义一般设 1000此时 1ms 等于 1 个 tick。如果设成 100就要像上面的代码那样做除法不然所有延时都快 10 倍。3.2 任务划分与优先级设置uCOS-III 允许每个任务独立优先级数字越小优先级越高。这个项目里典型的任务划分不是「一个任务管显示一个任务管通信」这么简单因为 emWin 的窗口重绘不能被打断太久。实际跑下来比较稳的方案是显示任务独占一支触摸扫描用低优先级轮询串口通信放在中断下半部分。下面这张表是我在类似项目上验证过的任务优先级分配方式任务名优先级周期/触发方式栈大小说明GUI_Task310ms 周期6KBemWin 主循环GUI_Exec 和窗口消息处理Touch_Task520ms 周期2KB读取触摸芯片通过队列发坐标给 GUIuartTask7信号量触发2KB串口数据帧解析状态更新Idle_TaskOS_CFG_PRIO_MAIN - 1空闲1KB统计 CPU 使用率GUI 任务优先级最高是因为 RA8875 的 BTE 操作虽然不占 MCU但 emWin 的窗口管理器重绘需要持有 GUI 锁。如果触摸任务的优先级更高触摸读取频繁打断 GUI反过来还会因为GUI_X_Lock等待造成触摸任务阻塞表现就是「按下没反应松开才触发」。把 GUI 放在最高优先级配合互斥锁触摸消息通过GUI_TOUCH_StoreStateEx在锁外投递整个流程最顺。static void GUI_Task(void *p_arg) { OS_ERR err; GUI_Init(); WM_SetCreateFlags(WM_CF_MEMDEV); /* 每个窗口使用存储设备防止闪烁 */ MainWindow_Create(); while (DEF_TRUE) { GUI_Exec(); /* 处理窗口消息和重绘请求 */ GUI_X_Delay(10); /* 让出 CPU同时保证刷新率 */ } }GUI_Exec是 emWin 的事件循环入口它会把所有待处理的 WM 消息和外设输入事件分发到对应窗口回调。这个循环不能阻塞所以延时用GUI_X_Delay而不是硬件定时器空等。WM_SetCreateFlags(WM_CF_MEMDEV)的作用是让每个窗口重绘时先在内存里完成绘制再一次拷贝到显存避免撕裂代价是每个窗口额外占用一块与窗口大小一致的内存。3.3 存储设备、内存池与显存的三层关系这个项目里存储设备、uCOS-III 的内存池和 RA8875 显存是三个独立的东西容易混淆。RA8875 的显存是它自带的 DDRAMMCU 通过 MRWC 寄存器按像素访问这部分不占 STM32 的 RAM。uCOS-III 的内存池是给任务、队列、信号量等内核对象用的。emWin 的存储设备则是从GUI_NUMBYTES分配出来的备用绘制画布。分辨率越高存储设备消耗越明显。比如 320x240 的 16bpp 窗口一个完整窗口的存储设备就是 320 * 240 * 2 150KB远超 STM32F103 的 SRAM所以实际项目里不会对整个主窗口启用 MEMDEV而是对画面中频繁变化的控件启用比如温湿度曲线、报警弹窗、软键盘。静态背景直接用 RA8875 的 BTE 拷贝贴图不经过 emWin。工程实践里我会在WM_PAINT消息里先判断区域是不是活动区域再决定用GUI_MEMDEV_Create还是直接绘制到屏上。emWin526 支持分段缓冲模式配置GUI_NUMBYTES只有 30KB 的时候曲线控件单独建一个高h / 4宽w的存储设备刷新时只重画这一块效果比整体WM_InvalidateWindow好得多。4. 源码头里的批处理脚本与分辨率配置思路4.1 MakeMovie.bat 和 Prep.bat 在工程里扮演什么角色拿到这份源码包第一眼看到一堆.bat文件可能以为只是开发者的临时工具实际它们是 emWin 的 PC 端资源转换工具链。emWin526 安装包里带有 BitmapConverter、FontConverter、MovieConverter 等 Windows 工具.bat就是在命令行下批量调用这些工具把 BMP 转成 C 数组、把字库转成.c文件。Prep.bat通常做的是清理输出目录、复制公共头文件、准备输入素材这些环境搭建工作MakeMovie.bat则是把一段动画素材或一组连续 BMP 帧转成 emWin 的 Movie 播放格式。典型内容长这样echo off rem Prep.bat准备输入输出目录 if exist output rd /s /q output mkdir output copy ..\Common\*.h output\ nul copy ..\Resources\*.bmp basedata\ nul echo [OK] resource preparedecho off rem MakeMovie.bat将 animation 目录内的 bmp 帧压成 emWin Movie ..\Utils\MovieConverter.exe -i animation -o output\animation.dta -r 16 ..\Utils\BitMapConvert.exe -i basedata -o output\images.c -b 16 echo [OK] movie generatedMovieConverter.exe -r 16指定颜色深度 16bpp-i和-o分别是输入输出路径。实际改工程时你不需要重新转素材除非换了屏幕背景图或者加了新图标那时把位图放到basedata目录再跑一遍这两个脚本生成的新.c文件会替换掉源码头里的同组文件。要注意 emWin 的资源是编译进 Flash 的图片越大 Flash 占得越多128KB Flash 的 F103C8 塞满一两张全屏背景图就报警了这时候优先把图片转成 RLE 压缩格式。4.2 多分辨率批处理与 RA8875 时序换算80x60.bat、200x150.bat、240x136.bat、240x180.bat、320x240.bat这批脚本本质上是为不同分辨率生成对应的 RA8875 初始化配置文件。RA8875 的显示窗口寄存器、像素时钟分频、HSYNC/VSYNC 参数都和分辨率严格相关手动改容易漏脚本批量生成最稳。脚本文件典型 LCD 尺寸像素时钟范围生成的配置头备注80x60.bat1.x 寸小屏3~6MHzlcdcfg_80x60.h低刷新率适合状态显示200x150.bat2.4 寸8~12MHzlcdcfg_200x150.h速度与功耗平衡240x136.bat2.4 寸宽屏9~14MHzlcdcfg_240x136.h常见于视频播放方案240x180.bat2.8 寸10~16MHzlcdcfg_240x180.h工控仪表常用320x240.bat3.5 寸15~20MHzlcdcfg_320x240.h需要 FSMC 配合才能流畅这批脚本的生成逻辑并不复杂RA8875 的 H/V 时序参数以像素数为基础数据手册有一组计算表格脚本内部按分辨率代入公式算出寄存器值输出成lcdcfg_*.h。改工程时如果换了屏幕不需要动脚本源码手动改一行分辨率宏重新跑对应的.bat就行。有一点提醒这条发布链路生成的参数是按「通用 40-pin RGB 屏」算的如果你的屏是 SPI 接口的 RGB 屏得手动微调像素时钟。另外脚本生成的配置文件里 HDPR/VDPR 如果出现 0说明分辨率算出来超出 RA8875 最大支持范围。4.3 工程目录编排与内存占用分析源码包里的结构沿用了嵌入式工程惯用的分层方式BSP 层放 LCD、Flash、触摸驱动GUI 层放 emWin526 库和配置uCOS-III 是完整内核源码App 层是任务函数、窗口回调、业务逻辑。.bat脚本放在Tools目录生成的资源文件输出到我们自己约定的Generated目录避免和手写代码混在一起产生误提交。内存方面需要盯住两个文件编译生成的.map文件和启动文件里的堆栈设置。工程里 emWin 的内存池通过GUI_ALLOC_AssignMemory从静态数组分配不依赖堆uCOS-III 的任务栈用静态数组也不用堆。那么 STM32F103 的 RAM 分配就是启动文件里的 heap 只给标准库函数用Stack_Size设 1KB 足够Heap_Size设 8KB30KB 给 emWin 池4 个任务栈共约 12KBuCOS-III 内核对象和中断占 4KB剩下给全局变量。加起来没超过 64KB 就安全。内存不足时我一般看.map文件里GUI_Memory数组的地址和__initial_sp是否重叠用 keil 的peripheral window能直接观察。5. 验证与排错的三条实操路径5.1 用寄存器回读确认 RA8875 是否真正运行初始化之后不要直接调 emWin 的画圆函数去验证先用最底层的方式确认链路通。读SYSR寄存器0x10判断 bit7 是否为 1uint8_t sysr RA8875_ReadReg(0x10); if (sysr 0x80) { /* 硬件初始化正常可以进行 GUI_Init */ } else { /* 电源未就绪查 RESET 引脚、FSMC 总线、PWRR 配置 */ }这个检查点能在 10 秒内区分出问题是硬件连接、FSMC 配置还是 emWin 配置。如果 SYSR 返回 0x00极大比例是 FSMC 的地址线接错导致命令和数据没有落到 RA8875 的寄存器窗口里。把RA8875_ReadReg读到的值打印到串口连续读 10 次都稳定是 0x80基本排除电气噪声问题。另一个高频问题是 emWin526 运行时直接进入HardFault。这种崩溃最常见的是GUI_NUMBYTES分配过大超出了静态数组边界。排查方法是在GUI_Init前后打印GUI_GetMemorySize()和GUI_GetUsedMemory()确认使用量没有超过实际可用的 SRAM。5.2 闪烁、花屏与下载失败的根因排查画面闪烁先看是不是任务抢占导致。如果 GUI 任务优先级低于某些高速任务重绘过程中数据被改RA8875 的像素时钟跟不上就会出现横向撕裂。处理方式是提高 GUI 任务优先级或者给变化区域单独建 MEMDEV。水分波纹是像素时钟和 RA8875 内部 FIFO 不匹配调低 PCSR 分频即可。工程下载失败也是一类高频坑Flash Download failed时检查 BOOT0/BOOT1 跳线F103 的 BOOT0 拉高时进入 ISP 模式调试器无法正常下载。还有一个隐蔽原因如果屏幕驱动用了大数组初始化 320x240 的渐变背景Flash 占用直逼 128KB超出芯片容量下限编译器没报错但下载器校验不过。5.3 字库与 GT 层的一个高级优化技巧RA8875 的 GT 层文字绘制在不启用透明模式时会先填充背景色再画前景字模这会导致视觉上「文字压掉背后的曲线」。实用做法是把前景色设置为0x00、背景色设置为1:1 透明具体是把文本渲染的色彩寄存器里的透明位打开。操作上注意 RA8875 的颜色寄存器写 3 字节 RGB 顺序某些批次的芯片高低字节顺序与参考手册相反如果发现文字颜色和配置的反了做一个字节序交换的宏即可不用改业务代码。字库性能优化的另一个点是不要逐字调用RA8875_PutText尤其绘制大段中文数据时。常见做法是在 RAM 里开一块 16x16 的字模缓存把整行文字一次性写入 RA8875 的显存区利用 BTE 块拷贝自动连续更新刷新整屏 320x240 的文本从 120ms 降到 40ms 左右。配合 uCOS-III 的空闲任务统计和GUI_Exec的按需回调这个组合在 F103 上跑 800x480 的静态界面已经足够从容。本文还有配套的精品资源点击获取