ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103+RA8875+emWin+uCOSIII人机界面移植实战指南

STM32F103+RA8875+emWin+uCOSIII人机界面移植实战指南 简介这是一份针对STM32F103单片机的嵌入式GUI综合工程源码适合有一定MCU基础、希望学习RA8875驱动、emWin移植及uCOS-III多任务协同的开发者。项目完整集成了RA8875液晶控制器驱动、emWin526图形界面库与uCOS-III实时操作系统并利用板载字库实现中文文本显示可帮助读者快速掌握LCD图形界面系统的搭建思路。压缩包共291个文件以h头文件、c源文件为主另有bat批处理、s汇编、lib库、exe工具及工程配置文件涵盖驱动层、中间层与应用层代码整体约4.3MB结构清晰便于对照学习。已有199人学习下载适合用于毕设、课程设计或工业人机界面开发参考。通过阅读源码可以理解STM32F103与RA8875的硬件接口时序、emWin526在uCOS-III上的移植方法以及多任务下液晶显示与调度机制的配合具备较高实践参考价值。1. STM32F103 上的 RA8875 emWin526 uCOSIII 是怎么组合起来的一颗 STM32F103、一块带 RA8875 控制器的 TFT、一套 emWin526 图形库再叠一个 uCOSIII 实时内核。这个组合在今天的 MCU 开发里看着有点“复古”但在工业 HMI、仪器仪表、医疗设备操作面板上它仍然是一套出货量极大的成熟方案。原因是 F103 的成本和供应太稳定RA8875 把字库、2D 加速、显存管理全包了MCU 只负责逻辑和调度emWin526 又恰好是当年适配这种“MCU 外部 LCD 控制器”架构最稳的 GUI 版本。这篇文章直接把这条链路拆开从 RA8875 内建字库的访问时序到 emWin526 的驱动适配再到 uCOSIII 任务怎么划分才能不卡界面最后落到 LED 联动和下载排错。适合正在做 F103 人机界面移植、或者拿到类似工程源码不知道从哪看起的工程师。2. RA8875 板载字库的访问机制与 FSMC 驱动实现2.1 RA8875 的内建字库GB2312 内码到屏幕点阵的完整链路RA8875 的卖点之一就是内置字库。常见版本自带 GB2312 简体中文字库16x16 点阵和 ASCII 字库部分型号还有 BIG5、日文、韩文字库可选。MCU 不需要把点阵数据存到 Flash也不需要去 SPI Flash 里读字模只需要向 RA8875 的寄存器写入字符的 GB2312 内码芯片内部会自动完成取模、放大和显示。16x16 的 GB2312 全角字库大约 216KB如果放到 STM32F103C8T6 的 64KB Flash 里是放不下的这正是板载字库最核心的价值所在。文本模式下RA8875 的显示流程是先设置前景色和背景色再设置光标坐标然后向字符码寄存器写入目标字符。对 ASCII向 0x92 写一个字节对中文向 0x90 写 GB2312 内码高字节、0x91 写低字节。写完以后 RA8875 会自动把点阵渲染到显存同时光标移动到下一显示位置。这段过程完全在 RA8875 内部完成STM32F103 只在初始化时配置一次字库模式之后显示汉字几乎不占 CPU 时间。RA8875 的显存是芯片外挂的 SDRAM 或者内部 SRAMF103 通过 FSMC 总线访问它。也就是说MCU 侧看到的“写寄存器”“写显存”都是对 FSMC 地址空间的读写RA8875 则把这些访问转换成显存的像素操作。这一点和直接驱动裸屏完全不同驱动裸屏时 MCU 要把整帧 RGB 数据刷进显存而现在显存管理在 RA8875 内部MCU 只需要发指令和数据。2.1.1 为什么不用 emWin 的字体有的工程师会质疑emWin526 本身支持中文显示为什么还要依赖 RA8875 板载字库原因有两个。第一是存储成本emWin 要显示中文需要把字库放进 MCU 的 Flash 或外部存储F103 的 Flash 资源很紧张放一个全角字库会吃掉三分之一以上的空间换一个 1MB 的 F103 又拉高整机 BOM。第二是渲染效率emWin 在 F103 上做文本绘制需要逐像素写入显存RA8875 板载字库则是芯片内部一次点阵搬移刷新速度差距明显。所以这块板子的正确做法是界面上需要频繁变化的中文文本走板载字库emWin 只负责控件、窗口、2D 图形和带纹理的元素。2.2 FSMC 8080 接口RA8875 与 STM32F103 的连接与时序配置RA8875 支持 8080 和 6800 并行接口也可以走 SPI但在 F103 项目里最常用的是通过 FSMC 扩展出 8080 接口。接线方式非常固定FSMC_D0~D15 接 RA8875 的并行数据线FSMC_NE1 接片选 CSFSMC_NWE 接写使能 WRFSMC_NOE 接读使能 RDFSMC_A0 接寄存器/数据选择线 RS。RA8875 的数据总线既可以配成 8 位也可以配成 16 位RGB565 方案下几乎都选 16 位这样一次可以读写一个像素效率高。FSMC 的时序配置是整套系统能不能点亮屏幕的第一道关。F103 的 FSMC 在 NOR/SRAM 模式下有 AddressSetupTime、AddressHoldTime、DataSetupTime 三项参数对应 8080 接口的地址建立时间、地址保持时间和数据建立时间。RA8875 的时序要求不算苛刻典型配置如下参数取值说明AddressSetupTime1~2地址建立时间单位 HCLK 周期AddressHoldTime1地址保持时间DataSetupTime4~7数据建立时间太小容易花屏BusTurnAroundDuration0总线周转时间WriteOperationEnable允许写操作MemoryTypeNORSram使用 NOR/SRAM 模式MemoryDataWidth16数据总线宽度 16 位ExtendedModeDisable关闭扩展模式初始化代码用标准外设库 3.5.0 来写这是 F103 项目里最常见的库版本网上能搜到大量同款配置网上常说的“库 v3.50 下载”指的就是这一版FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef FSMC_NORSRAMTimingInitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); FSMC_NORSRAMTimingInitStructure.FSMC_AddressSetupTime 1; FSMC_NORSRAMTimingInitStructure.FSMC_AddressHoldTime 1; FSMC_NORSRAMTimingInitStructure.FSMC_DataSetupTime 4; FSMC_NORSRAMTimingInitStructure.FSMC_BusTurnAroundDuration 0; FSMC_NORSRAMTimingInitStructure.FSMC_CLKDivision 0; FSMC_NORSRAMTimingInitStructure.FSMC_DataLatency 0; FSMC_NORSRAMTimingInitStructure.FSMC_AccessMode FSMC_AccessMode_A; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAM1; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_NOR; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; 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_NORSRAMTimingInitStructure; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct FSMC_NORSRAMTimingInitStructure; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);代码里 Bank 选择FSMC_Bank1_NORSRAM1对应片选 NE1映射地址从 0x60000000 开始。RA8875 的寄存器地址通过 RS 线区分RS 接 A0 时0x60000000这个基地址上读写的都是寄存器0x60000000 2才是数据端口因为 16 位模式下地址会右移一位。写数据时矩阵是*(volatile uint16_t *)(RA8875_DATA_ADDR)。如果屏幕点亮后出现花屏或者颜色错乱优先把 DataSetupTime 调大这是最典型的 F103 最小系统排查点。RA8875 初始化完成后先读一下芯片 ID 寄存器确认通信正常。RA8875 的 ID 寄存器在 0x00 和 0x01默认值是 0x75。读回的 ID 不对问题基本不在 GPIO而是 FSMC 引脚复用冲突比如部分 STM32F103 型号上 PA11 被 USB 占用如果工程里同时开了 USB 和 FSMC 复用会干扰总线操作。遇到这种问题先把不用的外设关掉再逐个开启定位。3. emWin526 在 uCOSIII 上的移植任务划分与临界区3.1 emWin526 的显示驱动适配LCDConf 里的关键接口emWin526 在 STM32F103 上的移植核心工作量在 LCDConf.c 和 GUIConf.c。和直接在内部 RAM 上跑 GUI 的单片机不同F103 的显存在 RA8875 侧emWin 的所有像素操作都要通过 FSMC 转发到 RA8875 的显存地址空间。也就是说emWin 认为自己在操作一块 16bpp 的显示设备实际每个像素读写最终都落在 RA8875 的 0x60000000 地址段上。emWin526 的移植不需要重写整个驱动层只需要实现一组底层回调最常见的是这套接口LCD_X_Config负责告诉 emWin 屏幕的分辨率、颜色格式、显存偏移LCD_X_ShowDriver负责把底层读写函数注册给 emWin 的驱动模型。最小实现如下static void LcdWriteReg(uint16_t Reg, uint16_t Value) { *(volatile uint16_t *)(RA8875_REG_ADDR) Reg; *(volatile uint16_t *)(RA8875_DATA_ADDR) Value; } static void LcdWriteData(uint16_t Value) { *(volatile uint16_t *)(RA8875_DATA_ADDR) Value; } static uint16_t LcdReadData(void) { return *(volatile uint16_t *)(RA8875_DATA_ADDR); } void LCD_X_Config(void) { GUI_DEVICE_CreateAndAdd(GUIDRV_FLEXCOLOR, GUICC_565, 0, 0); GUIDRV_FlexColor_SetFunc(GUI_DEVICE_GetById(0), PortAPI, GUIDRV_FLEXCOLOR_F66708, GUIDRV_FLEXCOLOR_M16B0); }PortAPI里填充的是LcdWriteReg、LcdWriteData、LcdReadData这组函数指针F66708 表示这是一个 8080 并口控制器M16B0 表示 16 位像素。GUICC_565对应 RGB565 颜色格式RA8875 初始化时也必须配成 16bpp两边颜色格式不一致时会出现红蓝互换、颜色偏暗这类问题。如果屏幕方向要旋转emWin 层的旋转用GUI_SetOrientation处理不要在驱动回调里手动换坐标否则 emWin 的窗口管理器会算错触摸坐标。3.2 uCOSIII 时基分离SysTick 给系统TIM4 给 GUIuCOSIII 需要节拍时钟来驱动任务调度最常见的是使用 SysTick 定时器。但 emWin 也需要一个毫秒级时间基准GUI_X_GetTime()如果直接用OSTimeGet()返回 tick 计数时间分辨率就是系统节拍周期比如系统节拍配置成 1ms 看起来够用但 uCOSIII 的 tick 计数在任务切换时可能被延迟更新导致 GUI 动画时序抖动。ST 官方例程里普遍的做法是独立开一个自由运行的硬件定时器给 emWin 用一般选 TIM4因为默认工程里 TIM2、TIM3 常被用来做 PWM 或者编码器输入。void GUI_X_InitOS(void) { GUI_X_Lock(); GUI_X_Unlock(); } U32 GUI_X_GetTime(void) { return g_GUITickCount; } void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { g_GUITickCount; TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }g_GUITickCount在 TIM4 的中断里自增GUI_X_GetTime()直接返回这个值GUI_X_Delay()则写成带OSTimeDly的版本让出 CPU。这里的要点是 GUI 时间基准和 RTOS 时间基准必须物理隔离不能用同一个定时器又给 uCOSIII 做 tick 又给 emWin 计时除非你能接受两个模块共享一个 tick 计数带来的相位误差。F103 的 TIM4 挂在 APB1 总线上初始化时注意时钟树配置APB1 的预分频器如果不设置为 1TIM4 的时钟需要按倍频关系换算。3.3 GUI 任务模型单任务访问原则与总线互斥uCOSIII 下有多个任务同时跑但 emWin526 的 GUI 函数不是可重入的。常见的做法是只创建一个 GUI 任务所有 emWin 调用都在这一个任务里发生其他任务要更新界面时通过消息队列把“更新请求”发给 GUI 任务GUI 任务在消息循环里处理。这样做可以完全避免 emWin 内部的信号量依赖也简化了GUI_X_Lock和GUI_X_Unlock的实现直接空实现或者关调度器即可。任务优先级和栈大小的划分可以参照下面的方案这个表格是类似项目里经过验证的配置任务优先级栈大小说明Task_Led5128 字节LED 闪烁和状态指示Task_Touch7256 字节触摸扫描通过消息队列上报坐标Task_GUI91024 字节唯一的 emWin 访问者Task_Comm11512 字节串口通信保存数据后通知 GUI栈大小的单位是字4 字节GUI 任务的 1024 字意味着 4KBemWin 在重绘复杂界面时回调函数嵌套比较深加上wm窗口管理器和WM_Paint的内部栈消耗这个大小是底线。如果 GUI 任务里出现HardFault先加栈不要急着改调度算法。uCOSIII 的统计任务可以观察每个任务栈的使用水线把这个功能打开。FSMC 总线的互斥也要注意。如果系统里只有 RA8875 一个外设挂在 FSMC 上单任务访问 GUI 时不需要额外加锁但如果还有外部 NOR Flash、SRAM 或者其他并口设备就要用互斥信号量保护 FSMC 总线访问。RA8875 的 BTE 搬运是异步的启动一次 BTE 后芯片忙于内部读写此时 MCU 可以继续做别的事但访问 RA8875 的状态寄存器前要先确认 BTE 完成。在 uCOSIII 环境里等待 BTE 完成不能用while死循环会占满 CPU正确做法是轮询几次后调用OSTimeDly(1)让出调度。void RA8875_WaitBTE(void) { while (RA8875_IsBteBusy()) { OSTimeDly(1); } }这样实现以后BTE 等待期间外部通信任务、LED 任务都能正常运行界面上大量填充操作时不会出现串口丢帧或者按键没反应的情况。这也是在 RTOS 和裸机环境里驱动 RA8875 最大的行为差异之一。4. LED 联动与界面刷新PWM 背光、文本层叠加与消息队列4.1 用 TIM3 做 LED PWM 背光避免占用 RA8875 的资源LED 在这个工程里有两个角色一个是 LCD 背光一个是板级状态指示灯。背光控制不一定要用 RA8875 内部 PWM很多板子的背光电路设计成 GPIO 开关或者外部三极管驱动直接由 MCU 输出 PWM 更灵活。F103 上有多个定时器TIM3 的 CH1 输出 PWM频率选 1kHz 到 5kHz 比较合适人耳听不到电感啸叫亮度调节线性度也够。void LED_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_Period 100 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 50; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); } void LED_SetBrightness(uint8_t percent) { TIM_SetCompare1(TIM3, percent); }预分频器 72 减一、周期 100 减一TIM3 的时钟源是 APB1 的 72MHz这样 PWM 频率正好是 72MHz / 72 / 100 10kHz。TIM_SetCompare1写入占空比范围 0 到 100。背光和界面联动时用一个全局变量保存当前亮度然后用消息队列把亮度变化事件发给 GUI 任务。注意不要在中断里直接调TIM_SetCompare1除非你的 PWM 周期很短且任务不会同时写否则先用原子操作更新变量再在任务上下文里统一处理。4.2 状态 LED 与 GUI 事件通过消息队列解耦状态 LED 和 GUI 界面状态的同步是一个典型的 RTOS 应用场景。比如设备正在执行打印任务界面上显示“打印中”同时 LED 慢闪提示运行状态任务结束时界面跳回主菜单LED 恢复常亮。直接把 GPIO 翻转写在 GUI 回调里会导致 LED 闪烁频率受 GUI 刷新率影响逻辑也不干净。uCOSIII 的消息队列可以把这两件事彻底解耦。OS_Q LedQ; OS_Q GuiQ; typedef enum { EVT_PRINT_START, EVT_PRINT_DONE, EVT_KEY_PRESSED } AppEventType; void Task_Led(void *p_arg) { OS_ERR err; OS_MSG_SIZE size; void *msg; (void)p_arg; while (1) { msg OSQPend(LedQ, 0, OS_OPT_PEND_BLOCKING, size, 0, err); switch ((AppEventType)(uint32_t)msg) { case EVT_PRINT_START: LED_SetFlashMode(); break; case EVT_PRINT_DONE: LED_SetSolidOn(); break; default: break; } } }OSQPend 的第二个参数 0 表示无限等待只有收到消息任务才被唤醒没有消息时 LED 任务完全不吃 CPU。GUI 任务更新完界面元素后通过 OSQPost 向 LedQ 发一条事件LED 任务收到后改变闪烁模式。这样即使 GUI 在重绘高开销界面时产生了阻塞LED 的闪频依然由自己的任务节奏控制。4.3 硬件文本层叠加把中文交给 RA8875CPU 只画控件这个项目最有价值的界面优化手段是 RA8875 的图层叠加。RA8875 支持两个显示图层Layer1 和 Layer2 可以独立使能、独立设置显存地址两个图层通过叠加的方式同时显示。于是可以这样分配emWin 配置为 Layer1 的显示驱动绘制窗口、按钮、进度条等控件RA8875 的文本模式配置在 Layer2专门用于渲染中文文本和数字。整个界面刷新时emWin 只需要重绘控件区域汉字部分由 RA8875 内部自动处理MCU 侧的像素写入量大幅减少。void RA8875_TextLayerInit(void) { RA8875_SelectLayer(2); RA8875_SetTextColor(RGB565_WHITE, RGB565_BLACK); RA8875_SetCursor(0, 0); RA8875_TextMode(); } void RA8875_ShowChinese(uint16_t x, uint16_t y, uint16_t code_h, uint16_t code_l) { RA8875_SelectLayer(2); RA8875_SetCursor(x, y); RA8875_WriteReg(0x90, code_h); RA8875_WriteReg(0x91, code_l); }RA8875_SelectLayer(2)之后所有的字库操作都只作用在文本层不会干扰 emWin 正在操作的图形层。这种做法的收益在复杂界面上非常明显假设一个界面有 20 个中文标签每个字 16x16 点阵用 emWin 默认方式刷新需要写入 20 个字模数据而板载字库方式只需要 40 次寄存器写入。F103 的 FSMC 时钟跑在 72MHz 时这两个方案的速度差距可以到数倍。需要特别注意的是RA8875 的文本模式和图形模式是独立的初始化文本层时要先关闭滚动、关闭光标闪烁设置好文本显示区域。如果文本显示区域没有配置可能会出现写入了字符码但屏幕上看不到任何内容的情况这一项排错时要放在前面检查。5. 验证整套工程的 5 个关键点5.1 验证点一字库回读测试上电第一件事不是跑 emWin而是先验证 RA8875 字库通路。写一个函数在屏幕固定位置显示“液晶板载字库”六个字如果显示正确说明 FSMC、RA8875 文本模式、字库寄存器全部正常。如果显示乱码检查 0x8E 字库类型选择寄存器是否匹配芯片内置字库的编码有些 RA8875 芯片内建的是 BIG5 字库写入 GB2312 内码时会出现半个字或者错字。5.2 验证点二FSMC 时序的边界排查屏幕闪现、颜色错乱、刷新时有雪花点基本都是 FSMC 读写时序太紧。把 DataSetupTime 从 4 逐级调到 7观察 RA8875 的数据手册中 WR 周期和数据保持时间要求。反过来如果 FSMC 时序过大GUI 整体刷新会变慢此时优先检查 FSMC 的地址建立时间而不是降低 DataSetupTime。5.3 验证点三DAP 下载失败与启动模式检查工程跑飞一次之后经常出现 DAP 下载失败报错停在连接目标芯片阶段。F103 的 BOOT1 引脚状态会影响系统存储区映射很多调试板把 BOOT1 悬空或者被外部电路拉高时SWD 连接会不稳定。处理方法是用镊子短接复位电容让芯片保持复位状态点下载再松开。如果每次都失败检查 BOOT0 和 BOOT1 的电平组合从 Flash 启动要求 BOOT0 为 0、BOOT1 任意。程序里开了 IWDG 看门狗也会导致下载过程中芯片反复复位先按住复位再下载可以绕过。5.4 验证点四串口 1 和串口 3 的时钟差异调试日志尽量放在 USART1 上USART1 挂在 APB2 总线F103 的 APB2 最高 72MHz波特率分频误差小USART3 挂在 APB1APB1 最高 36MHz同样的波特率计算出来分频值误差更大高速如 921600 时误码率明显上升。如果工程里串口 3 打印数据显示乱码但逻辑正确先检查 APB1 的时钟配置再看串口分频寄存器算出来的实际波特率不要直接怀疑代码。5.5 验证点五emWin 内存水位与 GUI_Init 失败GUI_Init()返回非零值时最常见原因就是GUIConf.h里的GUI_NUMBYTES超出 F103 的剩余 RAM。F103ZET6 有 64KB SRAMRA8875 的显存不在其中emWin 的内存池主要被窗口对象和存储设备消耗。先把GUI_NUMBYTES设为 8KB跑通一个简单窗口后再用GUI_ALLOC_GetMaxUsedBytes()观察实际峰值据此调整到合理范围。如果工程同时用了大量WM_CreateWindow和回调函数内存碎片会加剧io 建议在WM_MULTIBUF_Enable(1)不可用的前提下尽量复用窗口。最后在 GUI 任务的 while 循环里加一个栈使用率打印确认不会在长时间运行后溢出。本文还有配套的精品资源点击获取
返回列表