- 嵌入式
- 嵌入式GUI
- 驱动开发
【免费下载链接】u8g2
U8glib library for monochrome displays, version 2
导读
本文基于 U8g2 仓库中的官方 DMA 性能测试工程(sys/arm/stm32l031x4/u8g2_dma_spi_2mhz),完整还原 STM32L031 驱动 UC1609 SLG19264 单色屏时,软件模拟 SPI(SW SPI)、硬件 SPI(HW SPI)与 DMA SPI 三种数据传输方式的实测帧率(FPS)与静态内存(BSS)占用,并深入解析 u8x8cb.c 中三个 byte 回调函数的底层实现差异。读完本文,你将掌握:如何在 Cortex-M0+ 上为 U8g2 接入 DMA SPI 传输、三种方式在 2MHz 与 32MHz 主频下的真实性能差距,以及全缓冲(full buffer)与页缓冲(page buffer)模式各自的内存代价,可直接复用到自己的 STM32 裸机工程中。
一、测试工程概览
该工程位于sys/arm/stm32l031x4/u8g2_dma_spi_2mhz/,是一个以gnu-arm-none-eabi工具链构建的裸机 C 工程,由以下几部分组成:
| 文件 | 作用 |
|---|---|
| main.c | 主程序:时钟切换、显示初始化、FPS 计测与循环刷屏 |
| u8x8cb.c | U8x8 回调层:GPIO/延时回调 + HW SPI / DMA SPI 字节回调 |
| delay.c | 基于 SysTick 的微秒级延时实现 |
| Makefile | GNU ARM 构建脚本(含upload/clean目标) |
| stm32l031x4.ld | 链接脚本 |
测试对象是UC1609 SLG19264(192×64 点阵)显示屏,其驱动定义与三种 Setup 函数(页缓冲_1、_2与全缓冲_f)位于 csrc/u8g2_d_setup.c。
二、实测性能数据:2MHz 与 32MHz 主频
工程 README(README.md)给出了两组权威实测数据。测试中 MCU 系统时钟分别运行在2MHz与32MHz,同一屏幕分别使用全缓冲构造器uc1609_slg19264_f与页缓冲构造器uc1609_slg19264_1,并切换三种 SPI 传输方式。
2.1 系统时钟 2MHz(HSI 4MHz 分频场景)
| Constructor | SysClk | Transfer | FPS | BSS (RAM) |
|---|---|---|---|---|
| uc1609_slg19264_f | 2MHz | SW SPI | 0.3 | 1692 |
| uc1609_slg19264_f | 2MHz | HW SPI | 4.7 | 1692 |
| uc1609_slg19264_f | 2MHz | DMA SPI | 5.0 | 1948 |
| uc1609_slg19264_1 | 2MHz | SW SPI | 0.3 | 348 |
| uc1609_slg19264_1 | 2MHz | HW SPI | 2.6 | 348 |
| uc1609_slg19264_1 | 2MHz | DMA SPI | 2.6 | 604 |
2.2 系统时钟 32MHz(PLL 场景)
| Constructor | SysClk | Transfer | FPS | BSS (RAM) |
|---|---|---|---|---|
| uc1609_slg19264_f | 32MHz | SW SPI | 6.0 | 1692 |
| uc1609_slg19264_f | 32MHz | HW SPI | 73.8 | 1692 |
| uc1609_slg19264_f | 32MHz | DMA SPI | 76.7 | 1948 |
| uc1609_slg19264_1 | 32MHz | SW SPI | 5.6 | 348 |
| uc1609_slg19264_1 | 32MHz | HW SPI | 39.6 | 348 |
| uc1609_slg19264_1 | 32MHz | DMA SPI | 40.7 | 604 |
说明:数据来源为工程 README 及 main.c 中的注释,二者完全一致。
2.3 数据解读
- SW SPI 是性能瓶颈:32MHz 主频下全缓冲模式仅 6.0 FPS,因为每个比特都要由 CPU 通过 GPIO 翻转产生,且 u8x8cb.c 中时钟/数据引脚翻转均依赖 CPU 逐位执行;
- HW SPI 带来数量级提升:32MHz 下全缓冲从 6.0 FPS 跃升至 73.8 FPS(约 12 倍),数据传输交给 SPI 外设硬件完成,CPU 仅负责逐字节写数据寄存器;
- DMA SPI 进一步榨干外设:32MHz 下全缓冲达到 76.7 FPS,比 HW SPI 再提升约 4%;页缓冲模式达到 40.7 FPS,比 HW SPI 的 39.6 略高;
- 页缓冲内存优势明显:
_1构造器 BSS 仅 348 字节(DMA 版 604 字节),而全缓冲_f需 1692 字节(DMA 版 1948 字节),多出的约 256 字节正是 u8x8cb.c 中 DMA 中转缓冲区dma_buffer[256]的占用; - 低主频下差异被 SPI 时序掩盖:2MHz 主频时 HW SPI 与 DMA SPI 差距极小(4.7 vs 5.0),此时瓶颈已不在传输本身,而在于整机执行帧绘制逻辑的速度。
三、硬件接线:STM32L031 与 UC1609 SLG19264
工程提供了手绘接线示意图与实物连线照片,二者相互印证:
实物照片中屏幕正显示STM32L031 02 MHz 069 FPS:005.1,即 2MHz 主频下 FPS 计测画面。SPI 四线接法(不含 I2C 的 SDA/SCL)对应关系如下:
| GPIO | Display | Color |
|---|---|---|
| PA14 | CD | brown(棕) |
| PA13 | CS | yellow(黄) |
| PA7 | MOSI | green(绿) |
| PA6 | Reset | white(白) |
| PA5 | SCK | purple(紫) |
该映射在 u8x8cb.c 的注释中与代码一一对应:PA5 作 SCK、PA7 作 MOSI(数据)、PA13 作 CS、PA14 作 CD(数据/命令选择)、PA6 作 Reset。需要留意的是,该工程未使用硬件片选,SPI1->CR1中开启了SSM | SSI(软件管理 NSS),CS 引脚完全由 GPIO 回调手动控制。
四、U8g2 回调机制与三种传输的实现剖析
U8g2 的显示驱动层通过两个回调把上层绘图指令最终落到硬件:
- byte 回调(
u8x8_byte_hw_spi、u8x8_byte_dma_spi等):负责U8X8_MSG_BYTE_SEND、U8X8_MSG_BYTE_START_TRANSFER、U8X8_MSG_BYTE_END_TRANSFER、U8X8_MSG_BYTE_SET_DC等消息(消息定义见 csrc/u8x8.h); - GPIO/延时回调:负责
U8X8_MSG_GPIO_SPI_CLOCK、U8X8_MSG_GPIO_SPI_DATA、U8X8_MSG_GPIO_CS、U8X8_MSG_GPIO_DC、U8X8_MSG_GPIO_RESET及各延时消息。
在 main.c 的initDisplay()中,通过切换u8g2_Setup_uc1609_slg19264_f的第三个参数即可在三者间切换:
// 软件模拟 SPI(注释状态) // u8g2_Setup_uc1609_slg19264_f(&u8g2, U8G2_R2, u8x8_byte_4wire_sw_spi, u8x8_gpio_and_delay_stm32l0_spi); // 硬件 SPI u8g2_Setup_uc1609_slg19264_f(&u8g2, U8G2_R2, u8x8_byte_stm32l0_hw_spi, u8x8_gpio_and_delay_stm32l0_spi); // DMA SPI(注释状态) // u8g2_Setup_uc1609_slg19264_f(&u8g2, U8G2_R2, u8x8_byte_stm32l0_dma_spi, u8x8_gpio_and_delay_stm32l0_spi);u8x8_byte_4wire_sw_spi是 U8g2 内置的软件 SPI 字节回调,直接调用U8X8_MSG_GPIO_SPI_CLOCK/U8X8_MSG_GPIO_SPI_DATA消息驱动 GPIO 翻转,对应 u8x8cb.c 中 PA5/PA7 的 BSRR 原子操作。
4.1 硬件 SPI:CPU 逐字节搬运
u8x8_byte_stm32l0_hw_spi(u8x8cb.c)的核心流程:
case U8X8_MSG_BYTE_SEND: data = (uint8_t *)arg_ptr; while( arg_int > 0 ) { while ( (SPI1->SR & SPI_SR_BSY) || (DMA1_Channel3->CNDTR != 0) ) // 等待上次传输完成 ; *(uint8_t *)&(SPI1->DR) = *data; // 逐字节写入数据寄存器 data++; arg_int--; } break;在U8X8_MSG_BYTE_INIT中完成 SPI1 时钟使能、PA5/PA7 复用模式配置(AF0)、SPI1->CR1主模式配置(MSTR | SSM | SSI,CPHA/CPOL 均为 0,即 SPI Mode 0),并明确 SPI 时钟分频由主频决定、未强制预分频(2MHz 场景无需分频)。
4.2 DMA SPI:外设自动搬运
u8x8_byte_stm32l0_dma_spi(u8x8cb.c)在 HW SPI 基础上引入 DMA1 通道 3:
- U8X8_MSG_BYTE_INIT:除 SPI1 外还使能
RCC_AHBENR_DMA1EN;通过DMA1_CSELR将通道 3 请求源选择为SPI_TX;配置CMAR(内存地址指向dma_buffer)与CPAR(外设地址指向SPI1->DR);并使能SPI1->CR2 = SPI_CR2_TXDMAEN(发送 DMA 使能); - U8X8_MSG_BYTE_SEND:由于回调返回后
arg_ptr指向的数据区会被覆盖,先手动拷贝到全局dma_buffer[256](注释明确说明memcpy反而更慢,因此使用循环赋值);随后重置通道:CCR = 0,CNDTR = arg_int(传输字节数),再置MINC(内存地址递增)、DIR(内存到外设)、EN(使能通道)启动传输; - 同步等待:在
U8X8_MSG_BYTE_SET_DC与U8X8_MSG_BYTE_END_TRANSFER处,均通过(SPI1->SR & SPI_SR_BSY) || (DMA1_Channel3->CNDTR != 0)等待 DMA 传输真正完成,随后才切换 DC 或拉高 CS。
关键结论(由源码结构推断):DMA SPI 的加速本质是"CPU 只负责一次内存拷贝 + 启动 DMA,剩余的逐字节 SPI 发送完全由 DMA 与 SPI 外设流水完成"。在本测试中 DMA 拷贝 256 字节的开销被外设级并发所抵消,因此在 32MHz 下获得约 4% 的额外提升;但代价是 256 字节的全局 DMA 缓冲区常驻 BSS——这正是第二节表格中 DMA 列 BSS 比 HW 列多出约 256 字节的直接原因。
4.3 页缓冲与全缓冲的内存差异
从 csrc/u8g2_d_setup.c 可以看到三个构造器的缓冲差异:
uc1609_slg19264_1:调用u8g2_m_24_8_1,仅分配 1 页缓冲(页高 8 像素);uc1609_slg19264_2:调用u8g2_m_24_8_2,2 页缓冲;uc1609_slg19264_f:调用u8g2_m_24_8_f,全缓冲,一次性容纳整幅 192×64 画面。
三者的刷新流程也不同:页缓冲用u8g2_FirstPage()/u8g2_NextPage()分页绘制(main.c 即采用此模式),全缓冲则用u8g2_ClearBuffer()/u8g2_SendBuffer()。实测数据显示:帧率上全缓冲明显优于页缓冲(76.7 vs 40.7),但代价是 BSS 从 604 字节涨到 1948 字节,在 RAM 紧张的 MCU 上需要权衡。
五、FPS 计测方法:SysTick 计时
main.c 的测速逻辑非常简洁,可复用到任何工程:
startUp()中配置 SysTick 每 10ms 触发一次中断(main.c),SysTick_Handler累加全局计数SysTickCount;- 主循环每完成一次完整刷屏
frame_cnt++,同时计算fps = frame_cnt*1000 / diff(diff为 10ms 刻度计数),再拆出整数与小数部分; - 屏幕上实时显示
STM32L031、系统主频(MHz)、计数器值与FPS:x.x(main.c)。
同时PA9被配置为输出并随每帧翻转,可用示波器/逻辑分析仪直接观测帧周期(main.c)。
六、系统时钟切换:HSI 与 PLL
测试覆盖 2MHz 与 32MHz 两个主频点,切换逻辑在setHSIClock()(main.c):
- 2MHz 场景:直接使用 HSI 4MHz 时钟(
setHSIClock()被注释,代码注释表明"2MHz uC Clock"); - 32MHz 场景:使能 HSI 并开启
RCC_CR_HSIDIVEN(HSI 4 分频 → 4MHz),随后配置 PLL:PLLMUL16 | PLLDIV2,即 4MHz × 16 ÷ 2 =32MHz,切换时钟源后调用SystemCoreClockUpdate()同步全局变量。
延时函数delay_micro_seconds()(delay.c)依赖SystemCoreClock换算系统 tick,因此 README 与代码均强调:必须先调用SystemCoreClockUpdate()再使用延时。
七、构建、烧录与复现实验
该工程使用 GNU ARM 工具链构建(Makefile),关键配置如下:
- 架构:
-mcpu=cortex-m0plus -mthumb,定义STM32L031xx,优化等级-Os; - 源码组织:
CSRC := $(wildcard *.c)收集本目录源文件,U8G2SRC := $(wildcard ../../../../csrc/*.c)全量编译 U8g2 的 csrc 源码,系统库取自../../stm32l031x6/stm32l0xx/(该目录同时存放 STM32L0 标准外设库的头文件与源文件); - 链接:使用 stm32l031x4.ld,并开启
--gc-sections裁剪未引用段。
常用命令:
make # 生成 u8g2_test.hex / .elf / .dis,并打印 size make clean # 清理所有生成物 make upload # 通过 stm32flash 以 115200 波特率写入串口设备(脚本内为 /dev/ttyUSB0)复现性能对比的完整步骤:
- 保持
u8g2_Setup_uc1609_slg19264_f(&u8g2, U8G2_R2, u8x8_byte_stm32l0_dma_spi, u8x8_gpio_and_delay_stm32l0_spi);为有效行,注释掉其他两个 Setup 调用; - 默认
setHSIClock()被注释时测 2MHz 场景;取消注释后测 32MHz 场景; - 将构造器换成
uc1609_slg19264_1并保持u8g2_FirstPage/NextPage循环,即可对比页缓冲数据; - 编译烧录后读取屏幕上的
FPS:x.x,并与第二节表格对照。
八、工程延伸与参考
- 同系列还有 sys/arm/stm32l031x4/u8g2_dma_spi_32mhz 与 sys/arm/stm32l031x4/u8g2_test 工程,可对比不同主频与不同显示驱动下的表现;
- UC1609 页/全缓冲 Setup 的定义集中在 csrc/u8g2_d_setup.c,DMA 用到的 SPI_TX 请求映射与 SPI 外设寄存器操作见 u8x8cb.c;
- 该工程最初针对 U8g2 issue #2564(DMA 性能讨论)而建立,实测结论即本文第二节的两组数据;
- 若需在 Linux 上快速验证 U8g2 的 SW/HW 传输差异,可参考 sys/sdl 下的 SDL 仿真示例,本工程则适用于真实硬件时序验证。
总结:对 Cortex-M0+ 这类资源受限平台,DMA SPI 是"用 256 字节 BSS 换约 4% 帧率提升"的划算选择,而 HW SPI 已是性价比最高的方案;若 RAM 紧张,_1页缓冲构造器配合 HW/DMA SPI 可在 348/604 字节 BSS 下获得接近全缓冲 50% 的帧率。实际选型时,请结合自身主频、可用 RAM 与显示刷新需求,参考本文第二节的完整数据表决策。
- 嵌入式
- 嵌入式GUI
- 驱动开发
【免费下载链接】u8g2
U8glib library for monochrome displays, version 2
相关推荐
ts-pattern 高级玩法:P.array、P.record、P.set、P.map 与可变元组匹配深度教程
ts pattern 高级玩法:P.array、P.record、P.set、P.map 与可变元组匹配深度教程 ts pattern 是 TypeScript
嵌入式嵌入式GUI驱动开发Gist插件与GitHub无缝集成:企业版API URL自定义教程
Gist插件与GitHub无缝集成:企业版API URL自定义教程 你是否在使用Sublime Text编辑器时,希望将代码片段快速分享到GitHub Gist
开发工具CANN/catlass Tile逐元素乘法
TileElemwiseMul 代码位置 https://link.gitcode.com/i/db1feec07d057aadc3bb048c308537b7
算子库人工智能深度学习高性能计算CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考