
1. 项目概述从字符到图形的跨越在嵌入式开发中尤其是单片机MCU项目里12864液晶屏是一个非常经典的外设。它价格低廉、接口简单能显示字符和简单的图形是很多电子爱好者、学生和工程师入门显示模块的首选。市面上很多12864屏都采用ST7920作为控制器它功能强大支持中文字库和基本绘图。然而很多朋友在顺利驱动字符显示后一旦切换到图形模式就会遇到各种“灵异事件”显示的图形错位、出现莫名其妙的噪点、甚至整个屏幕花屏。这往往不是硬件问题而是对ST7920图形显示机制的理解不够深入。我自己在多个项目里用ST7920驱动12864屏画过波形、显示过图标、做过简单的UI踩过不少坑。这篇文章我就把自己在图形显示部分积累的经验和教训系统地梳理一遍。核心目标就一个让你彻底搞懂ST7920的图形显示原理避开那些常见的陷阱写出稳定、高效的图形驱动代码。无论你是正在做课设的学生还是开发产品原型的工程师这些实战经验都能帮你节省大量调试时间。2. ST7920图形显示核心机制解析要驾驭ST7920的图形显示不能停留在“调用库函数”的层面必须理解其内部的内存组织和访问逻辑。这就像你要在一个仓库里存放货物必须先搞清楚仓库的货架是怎么编号的一次能搬多少箱货。2.1 显存GDRAM的“非连续”地址布局ST7920的图形显示数据存储在图形显示RAMGDRAM中。这块GDRAM对应着屏幕上128x64个像素点。但它的地址编排方式非常特殊是初学者最容易困惑的地方。关键点一屏幕被分为上下两半。ST7920将128x64的屏幕在逻辑上划分为上下两个区域每个区域是128x32像素。上半屏对应GDRAM的前半部分下半屏对应后半部分。你在初始化时必须通过指令通常是0x36开启“扩展指令集”并进入图形模式才能对GDRAM进行读写。关键点二地址是“垂直方向”编址的。这是最核心也最反直觉的一点。我们通常认为屏幕坐标是(X, Y)X是横坐标Y是纵坐标。但ST7920 GDRAM的地址是先确定垂直方向Y轴的“行”再确定水平方向X轴的“列”。具体来说你需要分两步设置地址设置垂直坐标Y Address通过指令指定你要操作的是上半屏还是下半屏以及在该半屏中的第几“行”。这里的“行”指的是垂直方向上的一个16像素高的块。设置水平坐标X Address接着设置水平方向的地址即从该“行”的左侧开始的第几“列”。每一“列”对应一个字节8个垂直像素。你提供的地址表正是这种编址方式的体现。我们以最常用的12864屏128列 x 64行为例来解读上半屏第一“行”地址0x80, 0x81, 0x82 ... 0x87 (共8个地址对应水平方向0-127列) 上半屏第二“行”地址0x90, 0x91, 0x92 ... 0x97 (共8个地址对应水平方向0-127列) 下半屏第一“行”地址0x88, 0x89, 0x8a ... 0x8f (共8个地址对应水平方向0-127列) 下半屏第二“行”地址0x98, 0x99, 0x9a ... 0x9f (共8个地址对应水平方向0-127列)如何理解这个表每个地址如0x80并不直接对应一个像素而是对应屏幕上一个垂直的8像素列的起始位置。地址0x80到0x87这连续的8个地址对应了屏幕最顶部的第一组16个像素行Y0~15中从左到右X0~127的128列。0x80是X0~15这16列2字节的起始地址0x81是X16~31这16列的起始地址以此类推。地址0x90到0x97则对应了第二组16个像素行Y16~31同样是屏幕的上半部分。下半屏Y32~63的编址方式与上半屏完全一致只是起始地址从0x88和0x98开始。实操心得千万不要尝试去记忆这个地址表。正确的做法是封装一个画点函数SetPixel(x, y)。在这个函数内部根据x, y坐标计算出对应的GDRAM地址和位。这是最可靠、最不易出错的方法。你的应用层代码只需要关心(x,y)坐标即可。2.2 数据写入的“16位”单位另一个关键机制是向GDRAM写入图形数据时必须以16位2个字节为单位进行连续写入。当你设置好垂直和水平地址后写入的第一个字节会填充当前垂直地址即那16像素高的“行”的上半部分8个像素较低Y坐标紧接着写入的第二个字节会填充下半部分8个像素。这意味着即使你只想修改一个像素点理论上也需要读出该地址对应的16位数据修改其中对应的位然后再将16位数据写回去。ST7920没有提供单字节或单像素的修改指令。3. 图形显示标准操作流程与避坑指南理解了核心机制我们就可以制定一个稳健的图形显示操作流程。这里每一步都有需要注意的细节。3.1 初始化与清屏在进入图形模式前必须进行正确的初始化。// 伪代码示例 void LCD_Init(void) { Delay_ms(50); // 等待液晶模块上电稳定 LCD_WriteCmd(0x30); // 选择基本指令集 Delay_ms(1); LCD_WriteCmd(0x30); // 再次确认8位数据接口 Delay_ms(1); LCD_WriteCmd(0x0C); // 开显示关游标 Delay_ms(1); LCD_WriteCmd(0x01); // 清屏DDRAM Delay_ms(2); // 清屏指令需要较长延时 LCD_WriteCmd(0x06); // 设定点自动右移整体显示不移动 Delay_ms(1); // 切换到图形模式 LCD_WriteCmd(0x34); // 关闭图形显示进入扩展指令集 Delay_ms(1); LCD_WriteCmd(0x36); // 打开图形显示并保持在扩展指令集 Delay_ms(1); }注意指令0x34和0x36是切换图形显示开关的关键。在后续写入GDRAM数据时我们会频繁用到它们。清空GDRAM至关重要这是避免“噪点”的第一道防线。新屏或模式切换后GDRAM内容可能是随机的。void LCD_ClearGDRAM(void) { uint8_t x, y; LCD_WriteCmd(0x34); // 关闭图形显示准备写入GDRAM for (y 0; y 32; y) { // 垂直方向32个“行”16像素/行 * 32 64行 // 设置垂直坐标 (0-31) LCD_WriteCmd(0x80 | y); // 设置水平坐标起始为0 LCD_WriteCmd(0x80); // 连续写入128列 * 2字节 256个字节的0x00 for (x 0; x 128; x) { LCD_WriteData(0x00); LCD_WriteData(0x00); } } LCD_WriteCmd(0x36); // 重新打开图形显示 }这个操作会将整个GDRAM填充为0确保屏幕全黑为后续绘图提供一个干净的画布。3.2 核心绘图操作写GDRAM的标准步骤当你需要更新一部分图形时必须遵循以下步骤这是保证显示稳定的“金科玉律”。步骤一关闭图形显示。在写入GDRAM数据之前发送指令0x34。这一步的目的是防止在数据写入过程中控制器混合新旧数据导致屏幕出现闪烁或撕裂的杂影。步骤二设置目标地址。按照先垂直Y、后水平X的顺序设置你要开始写入的GDRAM地址。// 设置垂直地址0-31对应屏幕的哪个16像素高行 LCD_WriteCmd(0x80 | (y_address 0x1F)); // 设置水平地址0-7对应该行内从左起的第几组16像素列 LCD_WriteCmd(0x80 | (x_address 0x07));步骤三连续写入16位数据。向数据口连续写入两个字节。这两个字节共同决定了屏幕上一列16个垂直像素的亮灭1亮0灭。第一个字节控制上方8个像素bit0对应最上方的像素第二个字节控制下方8个像素。步骤四重新开启图形显示。在完成一批GDRAM数据写入后发送指令0x36让新数据生效。避坑指南不要在每次写入2字节后都开关图形显示。这样会导致屏幕剧烈闪烁。正确的做法是在绘制一幅完整画面或一个局部区域前关闭一次图形显示在这个区域的所有数据都写入完毕后再打开一次。例如你要更新一个16x16的图标就在开始画这个图标前关闭显示画完图标的所有数据后再打开。3.3 封装实用的画点与画图函数基于以上原理我们可以封装出最基础的画点函数这是所有高级图形功能的基础。// 假设屏幕坐标x: 0~127 (从左到右), y: 0~63 (从上到下) void LCD_DrawPoint(uint8_t x, uint8_t y, uint8_t mode) { // mode: 1点亮0熄灭 uint8_t vertical_addr, horizontal_addr; uint16_t data; uint8_t high_byte, low_byte; uint8_t bit_pos; // 1. 计算GDRAM地址 vertical_addr y / 16; // 确定是第几个16像素行 (0-3) // 上半屏(0,1)和下半屏(2,3)的地址基值不同 if (vertical_addr 2) { // 上半屏: 第一行基址0x80第二行基址0x90 horizontal_addr (x / 16) (vertical_addr * 0x08); vertical_addr (vertical_addr 0) ? 0x80 : 0x90; } else { // 下半屏: 第一行基址0x88第二行基址0x98 horizontal_addr (x / 16) ((vertical_addr - 2) * 0x08); vertical_addr (vertical_addr 2) ? 0x88 : 0x98; } // 2. 计算在该16像素列中的具体位置 bit_pos y % 16; // 0~15 // 3. 读出该地址原有的16位数据 // 注意ST7920没有直接读GDRAM的指令这里需要我们在MCU端维护一个显存缓冲区。 // 我们假设有一个全局数组 LCD_GraphBuffer[64][16] 存储了屏幕数据。 // 这里简化处理直接操作这个缓冲区。 high_byte LCD_GraphBuffer[y/8][x]; low_byte LCD_GraphBuffer[y/8 1][x]; // 简化逻辑实际应根据缓冲区结构调整 data (high_byte 8) | low_byte; // 4. 修改对应的位 if (mode) { data | (1 bit_pos); } else { data ~(1 bit_pos); } // 5. 分解回两个字节 high_byte (data 8) 0xFF; low_byte data 0xFF; // 6. 更新MCU端缓冲区 LCD_GraphBuffer[y/8][x] high_byte; LCD_GraphBuffer[y/8 1][x] low_byte; // 7. 将更新后的16位数据写入液晶屏遵循开关显示流程 LCD_WriteCmd(0x34); // 关图形显示 LCD_WriteCmd(vertical_addr); // 设垂直地址 LCD_WriteCmd(0x80 | (x/16)); // 设水平地址 LCD_WriteData(high_byte); LCD_WriteData(low_byte); LCD_WriteCmd(0x36); // 开图形显示 }有了LCD_DrawPoint函数你就可以在此基础上构建画线、画矩形、画圆、显示位图等更高级的函数。强烈建议在MCU的RAM中开辟一个与GDRAM对应的显示缓冲区所有绘图操作先修改这个缓冲区然后在需要刷新屏幕时将缓冲区中有变化的部分批量写入液晶屏。这是解决闪烁、提高效率的标准做法。4. 常见疑难问题深度排查在实际项目中即使按照标准流程操作依然可能遇到奇怪的问题。下面是我总结的几个典型问题及其根因。4.1 图形显示错位或镜像现象画的图形位置不对或者左右、上下颠倒了。排查思路检查坐标计算首先怀疑画点函数里的坐标到地址的换算公式。重点检查x/16和y/16以及取余运算是否正确。可以用几个边界点如(0,0), (127,0), (0,63), (127,63)进行测试。检查字节/位顺序ST7920的数据位序。确认你写入的字节数据中最高位MSB对应的是该列像素的左方还是右方有些屏的物理连接可能导致顺序相反。同样在一个字节内是最高位对应上方像素还是最低位对应上方像素这需要在LCD_DrawPoint函数中修改位操作1 bit_pos来适配。核对初始化指令确认发送的0x36开图形显示指令是否确实被执行没有和其他指令冲突。4.2 屏幕出现固定位置的噪点或线条现象每次清屏或绘图后屏幕某些固定位置总有零星亮点或暗线。排查思路GDRAM未彻底清空这是最常见的原因。确保你的清屏函数遍历了所有GDRAM地址垂直0-31水平0-7并且每个地址都写入了2字节的0x00。检查循环边界条件。写入过程中的干扰在写入GDRAM数据时是否严格遵循了“先关显示、再写数据、最后开显示”的步骤如果在写入过程中显示未关闭正在刷新的屏幕可能会采样到不完整的数据产生噪点。电源噪声ST7920对电源质量比较敏感。检查VCC电压是否稳定在5V或3.3V视模块而定尤其是在MCU和LCD同时动作时用示波器看看电源线上是否有毛刺。在电源引脚就近增加一个10uF电解电容并联一个0.1uF瓷片电容往往有奇效。4.3 显示内容闪烁或拖影现象画面更新时整个屏幕或局部有明显的闪烁或者旧图像的残影迟迟不消失。排查思路开关显示过于频繁如前所述避免在绘制每个点或每小块区域时都开关图形显示。应该以“帧”或“区域”为单位进行开关。未使用显示缓冲区直接操作GDRAM速度慢且由于需要开关显示必然导致闪烁。引入MCU端的显示缓冲区后你可以在后台准备好一整帧图像然后快速、连续地更新GDRAM只需开关显示一次从而极大减少闪烁。延时不足ST7920执行指令需要一定时间us级别。在连续发送指令和数据时如果没有插入足够的延时Delay_us(40)左右可能导致控制器未就绪数据丢失或错乱产生不可预知的显示效果。检查你的LCD_WriteCmd和LCD_WriteData函数中是否有必要的忙检测或软件延时。4.4 关于“三”字显示异常与Keil BUG你原文中提到的问题非常经典它暴露了开发环境的一个隐蔽陷阱。现象在Keil MDK环境下使用ST7920的字符模式显示汉字“三”时屏幕无显示。根因这不是ST7920的错也不是你代码的错而是Keil C51编译器的一个历史遗留BUG。在Keil的字符编码处理中字符0xFD在某些中文字库中“三”字的编码可能涉及这个值会被错误地处理或忽略。解决方案安装补丁正如你所说到Keil安装目录下的C51\BIN文件夹中寻找一个名为PK51_…….exe的补丁文件并执行。或者从Keil官网下载最新的C51编译器更新包。编码绕过如果无法安装补丁一个变通的方法是在你的字库数组或字符串中避免直接出现0xFD这个字节。可以通过程序在发送前进行判断和替换。升级环境考虑使用更新的Keil版本或者尝试其他编译器如SDCC。经验之谈这个BUG提醒我们嵌入式开发中问题不一定出在硬件或你的算法上。工具链编译器、链接器、编程器本身的怪异行为也是需要考量的因素。当遇到完全无法用逻辑解释的现象时去搜索引擎里加上“Keil BUG”、“编译器 BUG”等关键词或许会有意外收获。5. 综合实战一个完整的图形显示测试程序下面提供一个比简单示例更健壮、带缓冲区的图形显示测试程序框架。它实现了清屏、画点、画线Bresenham算法、显示一个位图图标并演示了如何避免闪烁。// lcd_st7920_graph.h #ifndef __LCD_ST7920_GRAPH_H #define __LCD_ST7920_GRAPH_H #include stdint.h #define LCD_WIDTH 128 #define LCD_HEIGHT 64 // 显示缓冲区 (64行每行16字节。16字节*8位128列) extern uint8_t LCD_GraphBuffer[LCD_HEIGHT/8][LCD_WIDTH]; void LCD_Init(void); void LCD_ClearScreen(void); void LCD_RefreshArea(uint8_t x_start, uint8_t y_start, uint8_t width, uint8_t height); void LCD_DrawPoint(uint8_t x, uint8_t y, uint8_t mode); void LCD_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode); void LCD_DrawBitmap(const uint8_t *bitmap, uint8_t x, uint8_t y, uint8_t width, uint8_t height); #endif// lcd_st7920_graph.c #include lcd_st7920_graph.h #include delay.h // 你的延时函数头文件 #include hardware_spi.h // 你的硬件SPI或GPIO模拟接口头文件 uint8_t LCD_GraphBuffer[LCD_HEIGHT/8][LCD_WIDTH] {0}; // 底层写命令/数据函数 (需根据你的硬件接口实现) static void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_RS_LOW(); // 命令 SPI_WriteByte(cmd); // 或用GPIO模拟 Delay_us(50); LCD_CS_HIGH(); } static void LCD_WriteData(uint8_t dat) { LCD_CS_LOW(); LCD_RS_HIGH(); // 数据 SPI_WriteByte(dat); Delay_us(50); LCD_CS_HIGH(); } void LCD_Init(void) { // ... 初始化代码同上文 ... LCD_ClearScreen(); // 初始化后清屏 } void LCD_ClearScreen(void) { uint8_t i, j; // 1. 清空MCU缓冲区 for (i 0; i LCD_HEIGHT/8; i) { for (j 0; j LCD_WIDTH; j) { LCD_GraphBuffer[i][j] 0x00; } } // 2. 清空液晶屏GDRAM LCD_WriteCmd(0x34); // 关图形显示 for (i 0; i 32; i) { // 垂直32行 LCD_WriteCmd(0x80 | i); // 设置垂直地址 LCD_WriteCmd(0x80); // 设置水平地址起始为0 for (j 0; j 128; j) { // 水平128列 LCD_WriteData(0x00); LCD_WriteData(0x00); } } LCD_WriteCmd(0x36); // 开图形显示 } // 刷新屏幕指定区域 (从缓冲区到液晶屏) void LCD_RefreshArea(uint8_t x_start, uint8_t y_start, uint8_t width, uint8_t height) { uint8_t x, y; uint8_t vertical_addr, horizontal_addr; uint8_t start_col, end_col; uint8_t start_row_block, end_row_block; // 计算需要刷新的列范围 (以16像素列为单位) start_col x_start / 16; end_col (x_start width - 1) / 16; // 计算需要刷新的行范围 (以16像素行为单位) start_row_block y_start / 16; end_row_block (y_start height - 1) / 16; LCD_WriteCmd(0x34); // 关闭图形显示开始批量写入 for (y start_row_block; y end_row_block; y) { // 确定垂直地址基值 if (y 2) { vertical_addr (y 0) ? 0x80 : 0x90; } else { vertical_addr (y 2) ? 0x88 : 0x98; } LCD_WriteCmd(vertical_addr); for (x start_col; x end_col; x) { // 设置水平地址 LCD_WriteCmd(0x80 | x); // 从缓冲区取出两个连续的字节对应一列16个像素 // 注意缓冲区组织方式LCD_GraphBuffer[行组][列] // 行组 y*2 和 y*21 uint8_t high_byte LCD_GraphBuffer[y*2][x]; uint8_t low_byte LCD_GraphBuffer[y*2 1][x]; LCD_WriteData(high_byte); LCD_WriteData(low_byte); } } LCD_WriteCmd(0x36); // 打开图形显示 } void LCD_DrawPoint(uint8_t x, uint8_t y, uint8_t mode) { if (x LCD_WIDTH || y LCD_HEIGHT) return; uint8_t row y / 8; uint8_t bit y % 8; uint8_t col x; if (mode) { LCD_GraphBuffer[row][col] | (1 bit); } else { LCD_GraphBuffer[row][col] ~(1 bit); } // 注意这里只更新了缓冲区。为了效率可以标记该区域为“脏” // 在主循环中定时调用 LCD_RefreshArea 进行批量刷新而不是画一个点就刷一次屏。 } // Bresenham画线算法 void LCD_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode) { int dx abs(x2 - x1); int dy abs(y2 - y1); int sx (x1 x2) ? 1 : -1; int sy (y1 y2) ? 1 : -1; int err dx - dy; int e2; while (1) { LCD_DrawPoint(x1, y1, mode); if (x1 x2 y1 y2) break; e2 2 * err; if (e2 -dy) { err - dy; x1 sx; } if (e2 dx) { err dx; y1 sy; } } } // 显示一个位图位图数据是逐行从上到下每行从左到右每字节8像素MSB在最左 void LCD_DrawBitmap(const uint8_t *bitmap, uint8_t x, uint8_t y, uint8_t width, uint8_t height) { uint8_t i, j, byte_width; byte_width (width 7) / 8; // 计算每行占用的字节数 for (i 0; i height; i) { for (j 0; j width; j) { // 计算当前像素在位图数据中的位置 uint16_t byte_index i * byte_width (j / 8); uint8_t bit_index 7 - (j % 8); // 假设MSB在左 uint8_t pixel_value (bitmap[byte_index] bit_index) 0x01; LCD_DrawPoint(x j, y i, pixel_value); } } }// main.c 测试程序 #include lcd_st7920_graph.h // 一个16x16的笑脸图标位图数据 (MSB在最左) const uint8_t smiley_bitmap[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; int main(void) { System_Init(); // 系统初始化 LCD_Init(); // 液晶初始化 // 测试1: 清屏 LCD_ClearScreen(); Delay_ms(1000); // 测试2: 画边框 LCD_DrawLine(0, 0, 127, 0, 1); LCD_DrawLine(127, 0, 127, 63, 1); LCD_DrawLine(127, 63, 0, 63, 1); LCD_DrawLine(0, 63, 0, 0, 1); // 测试3: 画交叉对角线 LCD_DrawLine(0, 0, 127, 63, 1); LCD_DrawLine(127, 0, 0, 63, 1); // 测试4: 显示位图 LCD_DrawBitmap(smiley_bitmap, 56, 24, 16, 16); // 在屏幕中央显示图标 // 将所有在缓冲区中的绘图更新到液晶屏上 LCD_RefreshArea(0, 0, LCD_WIDTH, LCD_HEIGHT); while (1) { // 主循环可以在这里添加动态图形更新 // 例如让一个点移动 static uint8_t pos_x 10; LCD_DrawPoint(pos_x, 10, 0); // 擦除旧点 pos_x; if (pos_x 117) pos_x 10; LCD_DrawPoint(pos_x, 10, 1); // 画新点 // 只刷新移动点附近的小区域避免全屏刷新带来的闪烁 LCD_RefreshArea(pos_x-1, 9, 3, 3); Delay_ms(50); } }这个测试程序展示了从底层驱动到上层应用的完整链条。它采用了显示缓冲区机制LCD_DrawPoint等函数只操作缓冲区最后由LCD_RefreshArea函数将更改的部分同步到液晶屏。在main函数的循环中我们只刷新了一个小区域这比刷新整个屏幕要快得多从而实现了无闪烁的动画效果。这才是产品级代码应该有的样子。