GD32F103驱动LCD12864:ST7920串口模式完整移植与绘图优化 📅 发布时间:2026/9/16 12:53:36 👁 浏览次数: 简介LCD12864点阵液晶屏的串行控制驱动源码面向嵌入式系统开发者与电子设计初学者解决在单片机平台上通过串行接口如SPI或I2C控制该屏显示文本与简易图像的问题适合智能仪表、小型人机交互面板等显示场景。压缩包内包含两个文件一个C源文件集中实现字符显示、图片显示、光标定位和清屏等操作一个头文件提供接口声明方便程序调用整个压缩包大小约3KB结构简明便于快速检视与移植。目前已有183人浏览学习适合正在学习单片机外设驱动或希望直接复用显示逻辑的开发者。源码中涉及的串行时序控制、液晶像素位操作及字符到点阵的转换处理十分具体有助于理解液晶显示底层原理同时驱动代码采用分层写法接口清晰可较方便地移植到STM32、51等主流微控制器上也能作为课程设计或竞赛作品的功能模块。1. LCD12864 驱动最容易被忽略的起点先确认控制器是 ST7920 还是 KS0108LCD12864 在嵌入式圈子里被当成过气外设但一直到今天工控仪表、充电桩操作面板、办公设备的状态窗口里仍然大量使用它。它的好处很直白128×64 点阵能放下 4 行汉字供货稳定比同尺寸 TFT 便宜一个量级。真正的问题出在LCD12864这个叫法太笼统市面上至少有两套完全不同的控制器带中文字库的 ST7920 和不带字库、必须自己画点阵的 KS0108。两者的初始化命令、读写时序、引脚定义都不是一套网上代码混着抄十有八九抄完点不亮。这篇文章以 GD32F103 ST7920 串口模式为主线把从接线、初始化、显示汉字到绘图刷帧的完整驱动步骤拆开讲最后收在从 STM32 迁移到 GD32 时最容易踩的三个坑上。没有字库的 KS0108 用户也可以对照着看知道哪些环节需要换套路。2. LCD12864 引脚与 GD32F103 接线PSB 引脚决定串口还是并口模式2.1 先认清手上的模块ST7920 和 KS0108 的差异对照拿到一块 LCD12864第一步不是搜代码而是翻到模块背面看控制器丝印。ST7920 方案的芯片上印 ST7920、SPLC792引脚定义里一定有一个叫 PSB 的引脚KS0108 方案的芯片印 KS0108B 或 HD61202U没有 PSB取而代之的是左右两个片选 CS1 和 CS2。这个差异直接决定驱动代码结构ST7920 写一个字节前要先发同步字KS0108 则要先用片选决定操作左半屏还是右半屏再走 8080 并口时序。对比项ST7920 控制器KS0108 控制器内置汉字8192 个 GB2312 简体字无必须外部取模接口形式8 位并口 / 4 位并口 / 串行8 位并口CS1/CS2 分两屏硬件特征有 PSB 引脚有 CS1、CS2无 PSB初始化0x30 系列命令串并口统一0x3E 关显示、0x3F 开显示左右片选分别设显示汉字直接写两字节 GB2312 编码先在内存放好 16×16 字模再画点典型应用菜单、静态文本、低成本 HMI波形、曲线、需要局部高速刷新KS0108 的读写时序是标准 8080 并口RS 区分命令和数据RW 控制读写方向E 是使能脉冲。它的显存是 8 页×64 列每页 8 个像素行画点逻辑比 ST7920 直观但没有内置字库显示一个汉字就要自己准备 32 字节字模并做内存映射。如果手里的屏是 KS0108后面章节里发 0x30 初始化和0xF8/0xFA 串行写命令这两节可以直接跳过直接看绘图和帧缓冲部分的思想接口函数整体要换成并口版本。2.1.1 PSB 引脚并口与串口的选择原理ST7920 的 PSB 是高电平并行、低电平串行。并口模式需要 DB0~DB7、RS、RW、E 再加上片选一共十几根线串口模式只需要 CS、SCK、SID 三根数据线模块把 RW、RS、E 全部忽略掉。用 GD32F103 这类引脚资源紧张的单片机我通常选串口模式理由是接线少、故障点少而且 ST7920 串口速率做到 1MHz 到 2MHz 时更新一个普通菜单界面完全够用。代价是每次写命令或数据都要被拆成同步字 高四位 低四位三个 8 位段代码比并口啰嗦性能上限也低刷整屏图形时明显能看到从上到下的扫描过程。提示PSB 不能悬空。多数模块悬空默认串口但也有模块内部没有可靠下拉长时间运行后电平漂移导致花屏。上电前直接用电烙铁把 PSB 和 GND 短接。2.2 ST7920 串行接口时序0xF8/0xFA 三字节帧与 SCK 相位ST7920 串口不是标准 SPI是一套类 SPI协议。每一帧由三段组成每段都是 8 个 SCK 脉冲第一段是同步字固定以 11111 开头后面两位区分命令还是数据写命令发 0xF8写数据发 0xFA第二段发送目标字节的高四位第三段发送低四位低四位要左移到字节的高四位再发。举个例子发送命令 0xC0 时三段依次是 0xF8、0xC0、0x00。如果写数据 0x55三段是 0xFA、0x50、0x50。SCK 空闲时为低电平SID 在下降沿之后改变、上升沿被锁存所以最稳妥的模拟顺序是先拉低 SCK再调整 SID然后拉高 SCK。这正好对应 SPI Mode 0 的 CPOL0、CPHA0 相位但由于 ST7920 要求每一段 8 位之间有明确停顿硬件 SPI 连续发送时容易把三个段的边界吃掉后面第 5 章会专门展开。下面这段是 GD32F103 GPIO 模拟串行发送的完整实现。#include gd32f10x.h #define LCD_CS_PIN GPIO_PIN_0 #define LCD_SCK_PIN GPIO_PIN_1 #define LCD_SID_PIN GPIO_PIN_2 static void lcd_delay_us(volatile uint32_t us) { /* GD32F103 默认 108MHz空循环大约 4 个周期/微秒 */ volatile uint32_t i; while (us--) { for (i 0; i 4; i); } } static void lcd_send_bits(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { gpio_bit_reset(GPIOB, LCD_SCK_PIN); if (dat 0x80) { gpio_bit_set(GPIOB, LCD_SID_PIN); } else { gpio_bit_reset(GPIOB, LCD_SID_PIN); } lcd_delay_us(1); gpio_bit_set(GPIOB, LCD_SCK_PIN); lcd_delay_us(1); dat 1; } } static void lcd_write_byte(uint8_t is_data, uint8_t dat) { gpio_bit_reset(GPIOB, LCD_CS_PIN); lcd_send_bits(is_data ? 0xFA : 0xF8); lcd_delay_us(1); lcd_send_bits(dat 0xF0); lcd_send_bits((dat 0x0F) 4); gpio_bit_set(GPIOB, LCD_CS_PIN); lcd_delay_us(1); }这段代码的关键点有三个。第一lcd_send_bits 按 MSB first 一位一位发先改 SID 再拉高 SCK保证上升沿来临时 SID 已经稳定。第二lcd_write_byte 用 is_data 区分同步字命令传 0 发 0xF8数据传 1 发 0xFA后续两个数据段都只保留四位有效位位置对齐到高四位。第三CS 在整个三段的帧期间保持低电平帧结束拉高拉高动作会复位 ST7920 内部的串行状态机这样下一帧无论如何都能从同步字开始不会累积错位。延时不敏感GPIO 模拟到 5MHz 也能跑但建议保留 1us 级停顿给 ST7920 内部移位寄存器留处理余量。2.3 GD32F103 的 GPIO 初始化与 5V/3.3V 电平匹配串口模式只有三个信号GD32F103 侧全部配置为推挽输出即可。注意不要开复用功能因为这里不经过 SPI 外设直接用 GPIO 翻转。void lcd_gpio_init(void) { rcu_periph_clock_enable(RCU_GPIOB); gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, LCD_CS_PIN | LCD_SCK_PIN | LCD_SID_PIN); gpio_bit_set(GPIOB, LCD_CS_PIN); /* CS 默认拉高 */ gpio_bit_reset(GPIOB, LCD_SCK_PIN); gpio_bit_reset(GPIOB, LCD_SID_PIN); }GPIO 速度参数选 50MHz。ST7920 串口时钟实际是 1MHz 量级GPIO_MODE_OUT_PP 和 50MHz 输出速度足够如果选 2MHz 低速档边沿会变缓在长线上容易多翻转一次。初始化最后一步把 SCK 和 SID 都拉低避免上电瞬间这两个引脚产生上升沿毛刺被模块当成数据。电平匹配是串口驱动里最容易忽略的坑。很多 LCD12864 模块默认供电 5V此时 ST7920 输入高电平门槛约为 0.7×5V3.5V而 GD32F103 是高电平 3.3V 输出正好差临界值。示波器看波形幅值足够但模块内部采样点刚好落在不确定区表现是显示偶尔花屏、初始化有时成功有时失败。常见解决办法是看模块背面有没有 3.3V/5V 跳线有就把 VCC 切到 3.3V没有跳线就串接 74LVC245 做电平转换。不要用上拉电阻硬拉到 5VGD32 引脚不是 5V 容忍会通过保护二极管倒灌电流。2.4 接线清单与并口模式对比以 GD32F103 的 PB0/PB1/PB2 为例串口模式接线如下。LCD12864 模块引脚功能接 GD32F103VCC模块电源按跳线接 3.3V 或 5VGND地GNDPSB并/串选择GND强制串口CS片选PB0SCLK串行时钟PB1SID串行数据PB2BL/BL-背光BL 经 100Ω 电阻接 3.3VBL- 接 GNDVO对比度10k 电位器分压约 3.0V如果你手里的工程基于并口模式接线则完全不同PSB 接 3.3VDB0~DB7 接八个 GPIORS、RW、E 各占一个引脚CS 可以接地。并口的好处是每次只发一个字节写命令开销小绘图刷新快坏处是占 11 个 GPIO在 GD32F103 这种 LQFP48 封装上往往要牺牲一部分外设引脚。串口模式用三个引脚就能跑所以下面的初始化、显示、绘图代码全部基于串口模式展开。3. LCD12864 初始化序列与最小驱动代码GD32F103 显示第一行字符3.1 初始化命令表与执行顺序ST7920 上电后不是立即能接收数据电源稳定需要至少 40ms。之后连续发送三次 0x30把控制器内部状态机锁定到8 位数据长度、基本指令集。注意这里说的 8 位是针对内部并行总线串口模式下也必须发这些命令只是传输方式变成三帧。命令值作用建议等待功能设定0x308 位数据长度基本指令集100us显示开关0x08先关显示避免清屏闪烁100us清屏0x01清 DDRAM光标归位2ms输入模式0x06写数据后地址自动 1画面不移动100us显示开关0x0C显示开、光标关、反白关100us0x30 为什么发三次第一次可能落在上电不稳期第二次确保接口模式进入 8 位第三次作为状态确认。只发一次 0x30 也能点亮一部分屏但温度变化或供电波动时容易随机乱码三次是工程上的冗余习惯。0x01 清屏命令一定要等多于 1.6msST7920 清 DDRAM 是指针遍历操作不等它完成就继续写数据新内容会被清掉一部分表现是第一个字符能显示后面的消失。void lcd_init(void) { lcd_gpio_init(); lcd_delay_us(50000); /* 上电稳定手册要求 40ms */ lcd_write_byte(0, 0x30); lcd_delay_us(120); lcd_write_byte(0, 0x30); lcd_delay_us(120); lcd_write_byte(0, 0x30); lcd_delay_us(120); lcd_write_byte(0, 0x08); lcd_delay_us(120); lcd_write_byte(0, 0x01); lcd_delay_us(2000); lcd_write_byte(0, 0x06); lcd_delay_us(120); lcd_write_byte(0, 0x0C); lcd_delay_us(120); }这段初始化里的 120us 比手册典型的 72us 多留了余量。串口模式下每次命令要传三个 8 位段模块内部串转并需要额外时间余量太小会偶发丢命令。lcd_delay_us 用空循环实现是因为初始化阶段 SysTick 可能还没配置直接用库函数 delay_us 会死等等后面系统时钟初始化完可以替换成基于 SysTick 的版本。3.2 行地址映射与 GB2312 汉字发送显示文本时最关键的不是怎么发数据而是 DDRAM 地址怎么算。ST7920 内部 DDRAM 只排了两行每行 16 个半角字符但 12864 模块物理上要显示 4 行 16×16 汉字于是厂商把两行各拆成前后两半拼出四个显示行。行首地址是 0x80、0x90、0x88、0x98而不是很多人以为的连续地址。void lcd_set_cursor(uint8_t row, uint8_t col) { uint8_t addr; switch (row) { case 0: addr 0x80 col; break; case 1: addr 0x90 col; break; case 2: addr 0x88 col; break; default: addr 0x98 col; break; } lcd_write_byte(0, addr); } void lcd_put_char(uint8_t dat) { lcd_write_byte(1, dat); } void lcd_put_string(uint8_t row, uint8_t col, const char *s) { lcd_set_cursor(row, col); while (*s) { lcd_put_char((uint8_t)(*s)); s; } }col 的单位是半角字符列一行 16 个半角或 8 个汉字。设置地址时 col 直接加在行首地址上即可ST7920 会在写数据后自动递增所以连续发送一串字符不需要反复设置地址。显示汉字时直接把 GB2312 编码的两个字节依次通过 lcd_put_char 发出去例如温度的编码是 0xCE 0xC2 0xB6 0xC8。要注意的是编译环境源文件保存为 UTF-8 时中文字符串字面量是 UTF-8 编码ST7920 不认必须把源文件改成 GBK/GB2312 编码或者用 \x 转义字节数组。这个是小问题但排错时最容易忽略。int main(void) { lcd_init(); lcd_set_cursor(0, 0); lcd_put_string(0, 0, LCD12864 OK); lcd_set_cursor(1, 1); lcd_put_string(1, 1, \xCE\xC2\xB6\xC8 36.5); while (1); }第二行显示内容里前半段是 GB2312 字面量后半段是 ASCII两者混排时 ST7920 能自动处理用户不需要按字符宽度切换。注意行地址 0x90 起点的 col1 会把第一个汉字放在第二行第二列16×16 汉字实际占据 DDRAM 两个半角列后续 ASCII 字符会接着这个宽度排下去。如果不希望混排统一用全角空格占位。3.3 初始化失败时的排查顺序屏不亮或满屏乱码时按三查顺序走不要先怀疑命令值。第一查 CS用示波器测 CS 在发送期间是否确实拉低很多模块的 CS 引脚被误接到 VCC 或 3.3V整个串口状态机永远收不到帧头。第二查 SCK 波形正常一帧应该是三段各 8 个脉冲每段之间有明显空闲如果看到连续 24 个脉冲没有停顿说明 CS 没参与帧控制需要检查 lcd_write_byte 里 CS 拉高的时机。第三查 0x01 后的延时把 2ms 改成 5ms 再试。这三步能解决九成初始化问题。剩下的一成大概率出在电平匹配或 PSB 悬空回到 2.3 节处理。提示用逻辑分析仪抓串口波形时触发条件设 CS 下降沿采样率不低于 10MHz重点看 SID 在 SCK 上升沿前是否建立。ST7920 对上升沿前 200ns 的数据保持时间有要求GPIO 模拟很容易满足硬件 SPI 则不一定。4. LCD12864 绘图模式与帧缓冲GDRAM 地址映射和画点实现4.1 GDRAM 的结构32×16 个 16 位单元不是 128×64 位平面ST7920 的绘图模式是 LCD12864 区别于普通字符屏的核心能力但它的 GDRAM 组织方式和 KS0108 完全不一样。KS0108 的显存是 8 页×64 列每页对应 8 行像素ST7920 则把整个 128×64 屏幕划分成 32 行×16 列个单元每个单元是 16 位数据。垂直地址 0~31 中的每一个管理两行像素水平地址 0~15 中的每一个管理 8 列像素。一个 16 位字里高字节对应偶数像素行低字节对应相邻的奇数像素行。GDRAM 垂直地址覆盖显示行数据字节安排0第 0 行、第 1 行高字节画第 0 行低字节画第 1 行1第 2 行、第 3 行高字节画第 2 行低字节画第 3 行2第 4 行、第 5 行高字节画第 4 行低字节画第 5 行.........31第 62 行、第 63 行高字节画第 62 行低字节画第 63 行水平方向上hx0 对应列 0~7hx1 对应列 8~15依此类推hx15 对应列 120~127。每个字节内最高位对应这 8 列中最左边的一列。这个映射意味着想直接在 GDRAM 里改一个点必须先把所在的 16 位单元读出来修改再写回而 ST7920 串口模式下读 GDRAM 几乎没有实用价值。工程上通用的方案是在 GD32 内部维护一个 32×16 的 uint16_t 数组作为帧缓冲所有画点、画线、写字模先改这个数组需要上屏时再整体刷进 GDRAM一劳永逸避开读操作。static uint16_t fb[32][16]; void lcd_fb_clear(void) { memset(fb, 0, sizeof(fb)); } void lcd_fb_point(uint8_t x, uint8_t y, uint8_t color) { uint8_t vy, hx, bitpos; uint16_t mask; if (x 128 || y 64) { return; } vy y 1; hx x 3; bitpos 7 - (x 7); mask (uint16_t)1 bitpos; if (y 1) { if (color) { fb[vy][hx] | mask; } else { fb[vy][hx] (uint16_t)~mask; } } else { if (color) { fb[vy][hx] | (uint16_t)(mask 8); } else { fb[vy][hx] (uint16_t)~(mask 8); } } } void lcd_fb_flush(void) { uint8_t vy, hx; lcd_write_byte(0, 0x34); /* 进入扩充指令集关闭图形显示 */ for (vy 0; vy 32; vy) { for (hx 0; hx 16; hx) { lcd_write_byte(0, 0x80 vy); /* 设置垂直地址 */ lcd_write_byte(0, 0x80 hx); /* 设置水平地址 */ lcd_write_byte(1, fb[vy][hx] 8); lcd_write_byte(1, fb[vy][hx] 0xFF); } } lcd_write_byte(0, 0x36); /* 打开图形显示 */ }lcd_fb_point 中 y 1 是行号除以 2得到垂直地址x 3 是列号除以 8得到水平地址bitpos 用于定位这 8 列中的具体某一列。偶行数据放在 16 位字的高字节所以掩码要左移 8 位奇行数据放低字节直接用原掩码。刷新函数先发 0x34 把图形显示关掉再逐单元设置地址并写入两个字节最后发 0x36 打开图形显示。关掉图形显示再写是为了避免每个单元写入过程中叠加显示残影这个写完再开的顺序如果反过来屏幕会闪得厉害。4.2 画线函数Bresenham 在 128×64 上的简化实现有了画点函数画线可以直接用 Bresenham 算法。GD32F103 的主频对整数运算绰绰有余不需要查表优化代码短反而容易维护。static void lcd_fb_line(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color) { int16_t dx, dy, sx, sy, err; dx abs((int16_t)x1 - x0); dy abs((int16_t)y1 - y0); sx (x0 x1) ? 1 : -1; sy (y0 y1) ? 1 : -1; err dx - dy; while (1) { lcd_fb_point(x0, y0, color); if (x0 x1 y0 y1) { break; } if (2 * err -dy) { err - dy; x0 sx; } if (2 * err dx) { err dx; y0 sy; } } }这里的 Bresenham 实现把误差判断放在两个独立的 if 里而不是 else if这样在斜率接近 1 时能同时在一个像素步进上增加 x 和 y 的偏移保证线的连续性。x0、y0 参数用 uint8_t进入函数后转成 int16_t 做减法防止无符号数溢出。如果只需要水平线或垂直线可以单独写快速版本水平线循环调用 point 时 x 递增y 不变省去每次计算 dx、dy但 128×64 的点阵规模下通用版瓶颈在刷新函数画线本身消耗很小先保证正确性再优化不迟。4.3 16×16 汉字点阵字模的显示方法与取模参数绘图模式下显示自定义汉字字模数据通常来自 PCtoLCD2002。取模设置要固定为三项逐行式、每行 8 位、高位在前。这样导出的 32 字节数组每两字节对应汉字的一行 16 个点顺序与行号一致。如果你用列行式取模字节组织和屏幕像素方向不对应会导致字模整体旋转 90 度。void lcd_fb_put_hanzi(uint8_t x, uint8_t y, const uint8_t *font16) { uint8_t row, col; for (row 0; row 16; row) { uint8_t b0 font16[row * 2]; uint8_t b1 font16[row * 2 1]; for (col 0; col 8; col) { if (b0 (0x80 col)) { lcd_fb_point(x col, y row, 1); } if (b1 (0x80 col)) { lcd_fb_point(x 8 col, y row, 1); } } } }逐行式字模里 b0 是 16 点中的左 8 点b1 是右 8 点每字节最高位对应最左像素和 4.1 节 GDRAM 的位序定义一致所以不需要额外反转。x 建议取 8 的整数倍比如 0、8、16、24原因是当 x 不是 8 的倍数时字模字节的位会横跨两个水平地址单元lcd_fb_point 虽然能正确处理但每次刷新要额外计算脏矩形性能没有提升。y 则没有对齐要求任意 0~63 都能正确显示。文本模式与绘图模式的切换也要注意。文本模式用 0x30 基本指令集操作 DDRAM绘图模式用 0x34/0x36 扩充指令集操作 GDRAM。切换回文本模式后DDRAM 内容不会丢但地址指针停留在上次位置所以切回后要重新执行 lcd_set_cursor。很多网上代码绘图后文字乱码的根因就在这里漏了重设光标字符显示到了屏幕外的 DDRAM 区域。5. 从 STM32F103 移植到 GD32F103 的三个坑和局部刷新优化5.1 坑一GD32F103 的 108MHz 主频让空循环延时缩水GD32F103 最高主频 108MHz而 STM32F103 是 72MHz。从 STM32 工程移植时如果延时函数是参照 72MHz 写死的空循环在 GD32 上整个初始化流程会快 1.5 倍。ST7920 对命令间隔不敏感但 0x01 清屏后的等待如果从 1.6ms 缩到 1ms 附近低概率出现首屏乱码。正确的做法是配置 SysTick 后把所有延时统一替换为基于 SystemCoreClock 的版本或者至少把空循环次数按 108/72 比例放大。在还没有配置时钟树的裸机前期阶段lcd_delay_us 维持空循环没问题但要在头文件里用宏标注此参数依赖主频防止后续换芯片型号时踩坑。5.2 坑二硬件 SPI 驱动 ST7920 时CS 和字节边界容易粘连GD32F103 的 SPI0 可以轻松输出 9MHz 甚至 18MHz 时钟理论上能驱动 ST7920 串口实际却经常出现初始化成功后偶发乱码。原因在 ST7920 的帧格式它需要完整三段作为一个处理单位段与段之间最好有停顿而硬件 SPI 连续发送 0xF8、0xC0、0x00 三个字节时SCK 是无间隙连续翻转的CS 如果由软件一直拉低模块内部状态机会把三段数据当作一个连续比特流拼接偶尔错一位导致整帧报废。我试过的可靠方案是每发送一段就把 CS 拉高再拉低让模块状态机复位到帧头但这样一来 CS 翻转开销抵消了 SPI 的速度优势整屏刷新和 GPIO 模拟差不了太多。所以我的建议是串口模式就用 GPIO 模拟把 SPI 外设留给更需要它的外设。5.3 坑三VO 对比度电位器与背光调节互相干扰LCD12864 模块上通常有一个蓝色电位器丝印只标 VO。旋转它改变的是 ST7920 的液晶驱动电压不是背光亮度。有些廉价款模块把背光限流电阻也串进了同一条电源支路调 VO 时背光跟着变暗字符同时变淡造成电压不稳的错觉。正确的调理方式是VO 接一个独立 10k 多圈电位器VCC 到 GND 分压后滑动端接 VO背光 BL 串 100Ω 电阻接 3.3V。调试时先把 VO 拧到 VCC 侧上电看到屏幕有浅底色后再慢慢向 GND 侧旋转直到背景无残影、黑点不发虚。这一步调不好再正确的初始化代码都会显示成一片模糊。5.4 局部刷新用脏矩形把整屏刷新量降低一个量级整屏刷新要遍历 32×16 共 512 个单元每个单元至少 4 次串行传输GPIO 模拟下大约 10ms 以上。这是 ST7920 绘图模式做动态波形的主要瓶颈。解决思路是只刷发生变化的区域维护一个脏矩形记录最小和最大的 vy、hx 范围。typedef struct { uint8_t min_vy, max_vy; uint8_t min_hx, max_hx; } dirty_rect_t; static dirty_rect_t dr { 32, 0, 16, 0 }; static void dr_mark(uint8_t vy, uint8_t hx) { if (vy dr.min_vy) dr.min_vy vy; if (vy dr.max_vy) dr.max_vy vy; if (hx dr.min_hx) dr.min_hx hx; if (hx dr.max_hx) dr.max_hx hx; } void lcd_fb_flush_dirty(void) { uint8_t vy, hx; if (dr.min_vy dr.max_vy) { return; } lcd_write_byte(0, 0x34); for (vy dr.min_vy; vy dr.max_vy; vy) { for (hx dr.min_hx; hx dr.max_hx; hx) { lcd_write_byte(0, 0x80 vy); lcd_write_byte(0, 0x80 hx); lcd_write_byte(1, fb[vy][hx] 8); lcd_write_byte(1, fb[vy][hx] 0xFF); } } lcd_write_byte(0, 0x36); dr.min_vy 32; dr.max_vy 0; dr.min_hx 16; dr.max_hx 0; }在 lcd_fb_point 成功写入一个点之后紧接着调用 dr_mark(vy, hx) 把该点所在的垂直地址和水平地址并入脏矩形。画一条竖线时只有 1 个 hx 和若干 vy 被标记刷新量从 512 个单元降到几十个单元。做滚动波形时还可以进一步优化每次只把新增列扫进去历史列不重复刷新这样动态更新速度能提高一倍以上。对于纯静态菜单显示一次后不再调用任何刷新函数帧缓冲保留在 RAM 里备用CPU 完全释放出来处理业务逻辑。本文还有配套的精品资源点击获取