ST7565液晶屏画线与刷新优化:嵌入式图形显示核心实践 简介本资源是一份面向嵌入式开发初学者与单片机爱好者的ST7565图形液晶驱动实践代码聚焦于核心绘图功能——任意斜率直线的高效绘制与显示刷新。针对ST7565控制器128×64点阵、8位并行接口特性资源提供完整可移植的C语言实现涵盖初始化配置、行列地址映射、Bresenham画线算法、像素点写入及逐行扫描刷新等关键环节适用于C51或兼容MCU平台的小型手持设备、仪器仪表等低功耗显示场景。压缩包为RAR格式仅含1个核心文件st7565 line.c3KB代码结构清晰含必要注释便于理解底层时序控制与显存操作逻辑。目前已有126人学习下载读者可直接复用该画线模块集成至自有项目快速掌握单色LCD图形驱动开发要点并基于此扩展圆、矩形、字符等基础图形函数。1. 项目概述从一份ST7565画线程序压缩包说起最近在整理一个老项目的资料时翻出了一个名为st7565-line.rar的压缩包。这个文件名本身就充满了故事感ST7565是一款经典的单色点阵液晶屏驱动芯片画线和刷新则是嵌入式图形显示中最基础也最核心的两个操作。这个压缩包连同里面那个tell6gx这样看似随意的文件名瞬间把我拉回了多年前调试128x64点阵屏的时光。对于很多从单片机、STM32入门嵌入式开发的朋友来说ST7565驱动的LCD屏几乎是“启蒙老师”般的存在。它价格低廉、接口简单通常是SPI或并口但恰恰是这种简单让开发者必须亲手去实现每一个像素的操控从而深刻理解位图、帧缓冲、局部刷新这些图形显示的核心概念。这个项目标题和相关的热词精准地指向了一个非常具体且普遍的需求如何在资源受限的嵌入式设备上高效、稳定地在ST7565这类液晶屏上实现画线功能并处理好屏幕刷新避免闪烁和残影。这不仅仅是写几行画线算法那么简单它涉及到驱动芯片的底层通信、显存GRAM的管理、绘图算法的优化以及刷新策略的选择。无论是做智能家居的温控面板、工业手持设备的简单UI还是学生时代的电子课程设计只要你用过这类小屏大概率都曾为“画线刷新”这个问题挠过头。网上搜索到的热词如“qt桌面画线”、“vc在视频窗口上画线”代表了在PC端丰富资源下的图形编程而“微信小程序单选框”、“小程序商城”则展现了应用层UI开发的便捷。但与之形成鲜明对比的是“电脑没有及时显示文件操作需要按f5刷新”、“win11新建文件夹需要刷新才能看到”这些抱怨恰恰说明了“刷新”这个动作在计算系统中的普遍性和重要性。在嵌入式领域我们没有操作系统桌面环境那种自动的、智能的刷新机制一切都需要我们自己来掌控。st7565 画线刷新这个关键词组合正是将这两个世界的痛点连接在了一起我们既需要实现高级的图形功能画线又必须在极其有限的资源下以最聪明的方式管理屏幕的更新刷新。因此本文将围绕这个核心需求彻底拆解一份完整的ST7565画线程序应该包含哪些内容。我会从驱动芯片的工作原理讲起到帧缓冲的设计、Bresenham画线算法的嵌入式实现与优化最后深入到局部刷新、避免闪烁的策略以及实际调试中遇到的种种“坑”。无论你是正在为课设发愁的学生还是需要在产品中优化显示效果的工程师希望这篇来自“踩坑前线”的总结能给你带来直接的帮助。2. ST7565驱动芯片核心原理与初始化要在ST7565上画线首先得和它“对上话”。ST7565是一个13265位通常我们只使用其中的12864位的LCD驱动控制器它内部自带显存GRAM我们的任务就是通过单片机MCU的IO口或SPI总线向这片GRAM中写入数据控制器会自动根据GRAM中的数据来驱动液晶像素的亮灭。2.1 通信接口与硬件连接ST7565通常支持4线SPI和8位并行两种接口模式。对于绝大多数单片机项目SPI接口因其占用IO少、编程简单而成为首选。关键的几个引脚需要理解清楚SCLK串行时钟由MCU产生用于同步数据。SIDMOSI串行数据输入MCU通过这根线向ST7565发送命令或数据。CS片选低电平有效在通信期间必须保持低电平。A0或RS命令/数据选择这是一个非常关键的引脚。它告诉ST7565当前发送的是命令还是显示数据。通常高电平代表数据低电平代表命令。所有的初始化设置都通过命令完成而画点、画线操作最终写入的是显示数据。RST复位低电平复位用于初始化芯片状态。注意不同厂家、不同封装的ST7565模块引脚名称和排列可能略有差异。务必以你手中模块的数据手册Datasheet为准。我曾遇到过A0引脚被标记为“RS”或“DC”的情况原理相同但代码中的宏定义需要对应修改。硬件连接好后软件上首先要实现两个最基础的函数写命令(CMD)和写数据(DATA)。这两个函数的区别仅在于操作A0引脚的电平。// 假设引脚已通过宏定义 #define LCD_A0_PIN GPIO_Pin_1 #define LCD_A0_PORT GPIOA void LCD_Write_Cmd(uint8_t cmd) { GPIO_ResetBits(LCD_A0_PORT, LCD_A0_PIN); // A0置低表示命令 SPI_Send_Byte(cmd); // 通过SPI发送一个字节 } void LCD_Write_Data(uint8_t data) { GPIO_SetBits(LCD_A0_PORT, LCD_A0_PIN); // A0置高表示数据 SPI_Send_Byte(data); }2.2 关键初始化序列解析ST7565上电后处于一个不确定的状态必须通过一系列严格的命令序列进行初始化才能正常显示。这个序列通常包括硬件复位拉低RST引脚一段时间通常1ms再拉高。设置显示起始行命令0x40 | (line)。这个命令非常有用它可以实现屏幕的硬件滚动但在初始化时通常设为0。设置页地址Page AddressST7565的GRAM在逻辑上被分为8“页”Page每页对应屏幕的8行像素因为数据是纵向组织的一个字节代表一列的8个像素点。命令为0xB0 | (page)。设置列地址Column Address命令为0x10 | (col_high)和0x00 | (col_low)。列地址共9位需要分两次设置。ADC选择段驱动方向命令0xA0或0xA1。这决定了你写入的数据是从屏幕左侧开始显示还是右侧开始。通常使用0xA0正向。显示方向Common Output命令0xC0或0xC8。这决定了行的扫描顺序影响显示是正像还是镜像。通常使用0xC0。液晶偏压比Bias命令如0xA2。影响对比度通常用0xA2(1/9 bias)。电源控制一系列0x2C,0x2E,0x2F命令用于逐步开启内部电源电路。内部电阻率调节Regulator Ratio命令0x20 | (ratio)。这个值直接影响屏幕的对比度通常需要根据供电电压VCC和模块上的电阻网络来调整0x20到0x27可选需要实际调试。电子音量对比度设置命令0x81后跟一个字节的参数0-63。这是调节显示深浅最常用的命令。显示开/关命令0xAF开显示0xAE关显示。一个典型的初始化函数骨架如下void ST7565_Init(void) { // 1. 硬件复位 LCD_RST_LOW(); Delay_ms(5); LCD_RST_HIGH(); Delay_ms(5); // 2. 初始化命令序列 LCD_Write_Cmd(0xAE); // 先关闭显示 LCD_Write_Cmd(0x40); // 设置显示起始行 0 LCD_Write_Cmd(0xA1); // ADC选择段方向A1为反向根据屏幕实际效果调整 LCD_Write_Cmd(0xC0); // COM扫描方向C0为正向 LCD_Write_Cmd(0xA6); // 正常显示非反显 LCD_Write_Cmd(0xA2); // 设置偏压比 1/9 LCD_Write_Cmd(0x2F); // 开启内部电源电路 LCD_Write_Cmd(0x21); // 内部电阻率调节此值需调试 LCD_Write_Cmd(0x81); // 电子音量模式 LCD_Write_Cmd(0x30); // 电子音量值此值需调试 LCD_Write_Cmd(0xAF); // 开启显示 }实操心得初始化序列中内部电阻率和电子音量是两个最需要实际调试的参数。不同批次的屏幕、不同的供电电压最佳值都可能不同。如果初始化后屏幕全黑、全白或对比度极差首先检查这两个参数。我的经验是先用一个中间值如电阻率0x21音量0x20初始化然后写一个简单的测试图案再通过按键或串口命令动态调整这两个参数观察屏幕变化找到最清晰的状态。这个过程无法跳过是点亮屏幕的必经之路。3. 帧缓冲设计与画线算法实现直接操作ST7565的GRAM进行绘图每次画一个点都要发送地址命令和数据效率极低且无法实现复杂的图形操作和局部刷新。因此在MCU的内存中开辟一块与屏幕物理分辨率对应的“帧缓冲”Frame Buffer是所有高级图形操作的基础。3.1 帧缓冲的内存布局ST7565的GRAM是纵向8位组织的。这意味着在内存中我们用一个二维数组uint8_t framebuffer[PAGE][COLUMN]来表示屏幕更为直观。其中PAGE是页数对于64行像素的屏幕PAGE 64 / 8 8。COLUMN是列数通常是128。framebuffer[page][col]这个字节的8个位bit从低位到高位或高位到低位取决于扫描方向分别对应屏幕上第page*8 bit行、第col列的一个像素点。1代表点亮0代表熄灭。例如要在屏幕的 (x, y) 坐标处画一个点假设原点在左上角x向右y向下void FB_SetPixel(uint16_t x, uint16_t y) { if (x SCREEN_WIDTH || y SCREEN_HEIGHT) return; // 边界检查 uint8_t page y / 8; uint8_t bit y % 8; framebuffer[page][x] | (1 bit); // 将对应位置1 } void FB_ClearPixel(uint16_t x, uint16_t y) { if (x SCREEN_WIDTH || y SCREEN_HEIGHT) return; uint8_t page y / 8; uint8_t bit y % 8; framebuffer[page][x] ~(1 bit); // 将对应位清0 }有了帧缓冲任何绘图操作画点、画线、画圆、显示字符都只是在修改这个内存数组。修改完成后通过一个刷新函数将整个或部分帧缓冲同步到ST7565的GRAM中屏幕才会更新。3.2 Bresenham画线算法的嵌入式实现画线是二维图形的基础。在嵌入式系统中我们无法使用浮点数进行斜率计算那样速度慢且占用资源多。Bresenham画线算法是唯一的选择它只用整数加减法和位运算效率极高。算法的核心思想是误差累积。以斜率在0到1之间的线段为例其他象限可通过对称变换处理计算x方向的增量dx x1 - x0 y方向的增量dy y1 - y0。初始化误差项err 2 * dy - dx。从起点(x0, y0)开始在循环中每次x增加1。判断err的值如果err 0则y增加1同时err减去2 * dx。无论err正负err都加上2 * dy。在每次循环中在当前的(x, y)位置画点。以下是针对任意方向线段的通用整数实现void FB_DrawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y0) { int16_t dx abs(x1 - x0); int16_t dy abs(y1 - y0); int16_t sx (x0 x1) ? 1 : -1; int16_t sy (y0 y1) ? 1 : -1; int16_t err dx - dy; int16_t e2; while (1) { FB_SetPixel(x0, y0); // 在帧缓冲中画点 if (x0 x1 y0 y1) break; e2 2 * err; if (e2 -dy) { err - dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } }注意事项这个算法画出的线是“标准”的但在低分辨率的点阵屏上斜线可能会呈现明显的“阶梯”状锯齿。这是由像素离散性决定的无法避免。如果追求视觉效果可以考虑使用抗锯齿算法但会大幅增加计算量在ST7565这种小屏上通常得不偿失。我们的首要目标是正确和高效。3.3 画线功能的封装与优化一个健壮的画线函数库不应止步于此。我们需要考虑更多线宽基础的Bresenham算法只能画单像素宽的线。要画粗线一个简单的方法是画多条平行的单像素线或者以每个画出的点为中心再画一个小的实心矩形例如对于2像素宽的线可以画一个2x2的方块。线型虚线或点划线。可以在画点循环中加入一个计数器根据计数器值的奇偶性决定是否画点。裁剪如果线段起点或终点在屏幕外需要进行裁剪否则会访问非法内存。一个简单有效的做法是在FB_SetPixel函数内部做边界检查并直接返回如上文所示。虽然这会让画屏外线时多做无用计算但代码简单对于嵌入式应用通常可接受。优化技巧如果你需要频繁画水平线或垂直线可以专门为这两种情况写优化函数。水平线可以直接用memset或循环操作整行字节垂直线则需要操作同一列的不同页的字节的指定位也有优化空间。这能带来显著的性能提升。4. 高效刷新策略与闪烁消除实战这是st7565 画线刷新问题的核心矛盾点。如果我们每次画一条线就调用一次全屏刷新函数将整个帧缓冲发送到屏幕那么当连续画多条线或动态更新图形时屏幕会剧烈闪烁因为屏幕在不断地被完全擦除和重绘。4.1 全屏刷新与局部刷新全屏刷新将整个framebuffer[8][128]数组按页顺序全部发送给ST7565。这是最可靠、最通用的方法但数据量大128*81024字节速度慢。void LCD_Refresh_Full(void) { for (uint8_t page 0; page 8; page) { LCD_Write_Cmd(0xB0 | page); // 设置页地址 LCD_Write_Cmd(0x10 | 0); // 设置列地址高4位为0 LCD_Write_Cmd(0x00 | 0); // 设置列地址低4位为0 for (uint8_t col 0; col 128; col) { LCD_Write_Data(framebuffer[page][col]); } } }局部刷新只更新帧缓冲中发生变化的那一部分区域。这需要我们在修改帧缓冲时记录下发生变化的矩形区域脏矩形Dirty Rectangle。刷新时只发送这个矩形区域内的数据。4.2 脏矩形算法实现实现一个简单的脏矩形管理器定义两个变量记录脏区域的边界dirty_x1,dirty_y1,dirty_x2,dirty_y2。初始化为一个无效区域如x2x1。在FB_SetPixel,FB_DrawLine等所有会修改帧缓冲的函数末尾调用一个FB_MarkDirty(x, y)函数这个函数负责更新脏矩形边界将新修改的点包含进去。static int16_t dirty_x1 SCREEN_WIDTH, dirty_y1 SCREEN_HEIGHT; static int16_t dirty_x2 -1, dirty_y2 -1; void FB_MarkDirty(int16_t x, int16_t y) { if (x dirty_x1) dirty_x1 x; if (x dirty_x2) dirty_x2 x; if (y dirty_y1) dirty_y1 y; if (y dirty_y2) dirty_y2 y; // 注意脏矩形需要包含页的完整字节边界所以最好在这里就将其扩展到字节边界 dirty_y1 (dirty_y1 / 8) * 8; // 向下取整到页开始行 dirty_y2 ((dirty_y2 7) / 8) * 8 - 1; // 向上取整到页结束行 }在需要刷新屏幕时比如一帧图形绘制完成后调用LCD_Refresh_Partial()函数它根据脏矩形计算需要更新的页和列范围只发送这部分数据。void LCD_Refresh_Partial(void) { if (dirty_x2 dirty_x1 || dirty_y2 dirty_y1) return; // 无脏区域 uint8_t start_page dirty_y1 / 8; uint8_t end_page dirty_y2 / 8; uint8_t start_col dirty_x1; uint8_t end_col dirty_x2; for (uint8_t page start_page; page end_page; page) { LCD_Write_Cmd(0xB0 | page); LCD_Write_Cmd(0x10 | (start_col 4)); // 列地址高4位 LCD_Write_Cmd(0x00 | (start_col 0x0F)); // 列地址低4位 for (uint8_t col start_col; col end_col; col) { LCD_Write_Data(framebuffer[page][col]); } } // 刷新完成后重置脏矩形 FB_ClearDirtyArea(); }4.3 双缓冲与垂直同步对于动态图形如动画、实时曲线局部刷新能解决大部分闪烁问题。但如果图形更新非常频繁在MCU向GRAM写入数据的过程中屏幕控制器可能正在读取GRAM进行显示这会导致屏幕上半部分显示新帧下半部分显示旧帧产生撕裂现象。更高级的解决方案是双缓冲在MCU内存中开辟两个完整的帧缓冲framebuffer_front和framebuffer_back。所有绘图操作都在framebuffer_back后缓冲上进行。当一帧图形绘制完成后执行一个“交换”操作。这个交换不是拷贝数据太慢而是交换指向这两个缓冲区的指针。交换后立即将新的前台缓冲即刚刚绘制完成的后台缓冲的内容刷新到屏幕。双缓冲结合局部刷新可以几乎完全消除闪烁和撕裂。但它的代价是内存占用翻倍。对于ST7565的1024字节显存双缓冲就是2048字节这在一些RAM很小的8位单片机如ATmega328P只有2KB RAM上可能是沉重的负担。此时就需要在效果和资源之间做出权衡。踩坑实录我曾在一个基于STM32F103的项目中使用了双缓冲局部刷新。效果很好但后来发现系统偶尔会卡顿。用逻辑分析仪抓取SPI波形后发现当局部刷新的区域很大接近全屏时SPI数据传输会占用大量时间阻塞了主循环。解决方案是将刷新过程拆分成多个小块在主循环中分时发送或者使用DMA直接存储器访问来搬运数据到SPI外设让CPU腾出手来做其他事情。这是从“功能实现”到“性能优化”的关键一步。5. 程序架构与高级功能拓展一个完整的、可维护的ST7565图形程序不应该只是一堆散乱的函数。良好的架构能让后续添加字体、图标、UI控件变得轻而易举。5.1 分层驱动设计建议采用至少三层结构硬件抽象层HAL提供LCD_Write_Cmd、LCD_Write_Data、LCD_Delay_ms等最底层的函数与具体MCU型号和硬件连接方式相关。这一层的目的是隔离硬件变化。驱动层Driver实现ST7565的初始化、设置对比度、清屏、以及最核心的LCD_Refresh函数支持全屏/局部。这一层知晓ST7565的特定命令和GRAM结构。图形库层Graphics Library提供与硬件无关的图形API。它操作的是一个抽象的“画布”即帧缓冲。这一层包含SetPixel、DrawLine、DrawRect、DrawCircle、DrawChar、DrawString等函数。它不应该包含任何直接操作ST7565硬件的代码。这样当你需要把程序从STM32移植到ESP32或者换用另一款类似的LCD驱动芯片如SSD1306时你只需要重写HAL层和驱动层图形库层的代码可以完全复用。5.2 字体显示与中文字库在画线的基础上显示文字是下一个刚需。对于英文字母和数字可以使用位图字体Bitmap Font。每个字符定义一个字节数组字模表示其像素点阵。显示时将字模数据按位“或”到帧缓冲的相应位置即可。对于中文由于字符数量庞大需要外置Flash或从文件系统读取字库。通常使用GB2312等编码根据汉字内码计算出在字库文件中的偏移地址读取对应的点阵数据。这是一个相对独立且复杂的模块但核心思想与英文字体相同。优化技巧显示字符串时避免在循环中频繁计算每个字符的起始坐标。可以维护一个“光标”位置cur_x, cur_y每显示一个字符后光标自动右移对于等宽字体或根据字符宽度移动对于非等宽字体。这比每次都从头计算坐标要高效得多。5.3 简单UI框架与动画有了画图、画线、显示文字的基础就可以构建简单的UI了。一个最基础的UI框架可以包含控件基类定义位置、大小、绘制函数、事件处理函数。具体控件标签Label、按钮Button、进度条ProgressBar等继承基类并实现自己的绘制逻辑。消息循环在主循环中检测输入按键、触摸将消息分发给当前焦点控件。动画的本质是连续帧的快速刷新。例如要实现一个移动的小球在帧缓冲A中绘制背景和小球在位置1。刷新到屏幕。等待一个很短的时间间隔如16ms对应约60FPS。在帧缓冲B中绘制背景和小球在位置2先清空小球旧位置画上新位置。交换缓冲并刷新。 如此循环。使用双缓冲和局部刷新只更新小球移动前后的区域对于流畅动画至关重要。6. 调试技巧与常见问题排查调试嵌入式显示问题眼睛看和工具测要结合。6.1 常见问题速查表现象可能原因排查思路屏幕全白/全黑无任何显示1. 电源或背光问题。2. 初始化序列错误或未执行。3. 对比度设置极端0x81命令参数为0或63。4. 硬件连接错误如A0、RST引脚未正确控制。1. 用万用表测量VCC、GND、背光电压。2. 用逻辑分析仪或示波器抓取SPI总线确认初始化命令序列被正确发送。3. 尝试动态调整对比度参数。4. 检查所有控制引脚的逻辑电平。显示乱码有亮点但非预期图案1. 帧缓冲数据与GRAM映射关系错误。2. 扫描方向ADC/Common Output设置反了。3. 字节内像素位顺序LSB/MSB错误。1. 写一个简单的测试图案如棋盘格、对角线到帧缓冲全屏刷新看图案是否对应。2. 尝试交换0xA0/0xA1和0xC0/0xC8命令。3. 检查FB_SetPixel函数中(1 bit)的移位方向。画线时线不连续或位置偏移1. 画线算法实现有误特别是误差项处理。2. 坐标系统转换错误如原点位置。3. 屏幕物理像素排列与逻辑坐标不符。1. 用已知点如(0,0)到(127,63)的对角线测试算法。2. 确认你的坐标系原点通常是左上角和方向。3. 查阅屏幕模块手册确认其像素排列。刷新时屏幕闪烁严重1. 未使用任何缓冲策略直接写GRAM。2. 全屏刷新频率过低且不规律。3. 刷新过程中被打断。1. 引入帧缓冲确保绘图完成后再统一刷新。2. 尝试使用局部刷新减少数据量。3. 如果使用RTOS确保刷新任务具有足够高的优先级和稳定的周期。动态图形有撕裂感1. 单缓冲下刷新过程被屏幕扫描中断。2. 局部刷新区域计算错误未覆盖全部变化区域。1. 考虑使用双缓冲。2. 检查脏矩形标记逻辑确保它正确包含了所有被修改的像素。运行一段时间后花屏或死机1. 内存溢出帧缓冲越界访问。2. SPI通信时序不稳定导致数据错位。3. 堆栈溢出。1. 在所有数组访问处加强边界检查。2. 降低SPI时钟频率测试。3. 检查编译器生成的map文件优化内存使用。6.2 实用调试工具与方法逻辑分析仪这是调试SPI/I2C通信的神器。可以清晰地看到发送的每一个命令和数据字节以及时序是否符合ST7565数据手册的要求如建立时间、保持时间。很多初始化问题靠猜是没用的必须抓波形看。串口打印在关键函数入口、错误分支处添加串口打印信息输出帧缓冲大小、脏矩形坐标、画线参数等可以帮助理解程序流程。模拟器在PC上如用SDL库模拟一个128x64的显示窗口将你的图形库代码去除硬件相关部分移植上去运行。这可以极大地加快算法开发和UI设计的迭代速度避免每次修改都烧录到单片机。测试图案编写几个简单的测试函数如全屏填充、棋盘格、渐变灰阶条。这些图案能快速帮你判断显示的基本逻辑是否正确。回过头来看st7565-line.rar这个压缩包它可能包含了从最底层驱动到上层画线函数的所有代码也可能只是一个简单的、未经优化的实现。但无论其内容如何它所代表的问题——如何在有限的资源下驾驭一块简单的屏幕画出稳定流畅的图形——是每一个嵌入式图形开发者成长的必经之路。通过理解原理、设计缓冲、优化刷新我们不仅能解决ST7565的问题这套方法论同样适用于SSD1306、ILI9341等更复杂的显示屏。从点亮第一个像素到实现一个流畅的动画界面这个过程本身就是对嵌入式系统资源管理、实时性处理最生动的实践。本文还有配套的精品资源点击获取