12864液晶自定义图形显示实战(C代码)

📅 发布时间:2026/7/15 5:39:15 👁️ 浏览次数:
12864液晶自定义图形显示实战(C代码)
1. 12864液晶屏图形显示基础第一次接触12864液晶屏时我被它那密密麻麻的引脚吓到了。后来才发现这个128x64点阵的小家伙其实是个像素艺术家——每个点都像画布上的像素通过C语言代码就能让它画出任意图形。和常见的1602字符液晶不同12864最大的特点就是能显示自定义图形从公司Logo到动态图标都不在话下。这块屏幕内部藏着两个关键区域DDRAM显示数据RAM和GDRAM图形显示RAM。DDRAM主要用来存储预定义的字符而我们要玩的GDRAM才是自定义图形的舞台。想象GDRAM是个128列×64行的网格本每个格子对应屏幕上一个像素点。向GDRAM写入1对应像素点亮写入0则熄灭。实际开发中常用两种控制芯片ST7920和KS0108。我手头这块是基于ST7920的它有个贴心设计——内置中文字库但今天我们重点玩转它的图形模式。上电初始化时必须按照特定时序发送指令序列void LCD_Init() { DelayMs(50); // 等待液晶稳定 WriteCmd(0x30); // 基本指令集 WriteCmd(0x0C); // 开显示关光标 WriteCmd(0x01); // 清屏 WriteCmd(0x06); // 光标右移 }2. 图形数据提取与转换实战要让液晶显示自定义图案首先得把图像转换成单片机认识的语言。这个过程就像把照片转成马赛克画我用的是PCtoLCD2002这款取模软件。具体操作时踩过几个坑图像必须处理成单色BMP格式尺寸最好就是128x64像素取模方向要选纵向取模字节倒序ST7920的特性输出格式选C51格式方便直接嵌入代码比如要显示一个心形图标在取模软件中设置参数后会得到类似这样的数组const unsigned char heart[] { 0x00,0x00,0x38,0x7C,0xFE,0xFE,0xFE,0x7C, 0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //...更多数据... };每个十六进制数代表8个像素点的状态比如0xFE对应二进制11111110表示连续7个亮点和1个暗点。我曾因为取模方向设错显示出来的图像都是碎片调试了半天才发现问题。3. GDRAM操作深度解析ST7920的GDRAM寻址方式很有特点它把屏幕分成上下两半区各32行每个半区需要单独设置地址。具体寻址指令void SetGDRAMAddr(unsigned char x, unsigned char y) { WriteCmd(0x34); // 启用扩展指令集 WriteCmd(0x80 | y); // 设置垂直地址行 WriteCmd(0x80 | x); // 设置水平地址列 WriteCmd(0x30); // 切回基本指令集 }写入图形数据时有个关键细节每个地址需要连续写入两个字节的数据。我最初没注意这点导致显示总是错位。正确的写入流程应该是先设置起始地址写入高8位数据写入低8位数据地址自动加1准备下一个点完整的数据写入函数如下void DrawImage(const unsigned char *img) { unsigned char x,y; for(y0; y32; y) { // 上半屏32行 SetGDRAMAddr(0, y); for(x0; x16; x) { // 每行16字节×8点128点 WriteData(img[y*32 x*2]); WriteData(img[y*32 x*2 1]); } } // 下半屏同理... }4. 完整图形显示驱动实现结合上述知识我们可以封装出更易用的图形显示函数。首先需要基础通信函数void WriteCmd(unsigned char cmd) { while(CheckBusy()); // 等待液晶就绪 LCD_RS 0; // 命令模式 LCD_RW 0; // 写入 LCD_DATA cmd; // 数据总线 LCD_EN 1; // 使能脉冲 DelayUs(10); LCD_EN 0; } void WriteData(unsigned char dat) { while(CheckBusy()); LCD_RS 1; // 数据模式 LCD_RW 0; LCD_DATA dat; LCD_EN 1; DelayUs(10); LCD_EN 0; }然后实现图形显示的核心函数void ShowFullScreenImage(const unsigned char *img) { unsigned char i,j; WriteCmd(0x34); // 扩展指令集 // 上半屏写入 for(i0; i32; i) { WriteCmd(0x80 | i); // 行地址 WriteCmd(0x80); // 列起始地址 for(j0; j16; j) { WriteData(img[i*32 j*2]); WriteData(img[i*32 j*2 1]); } } // 下半屏写入 for(i32; i64; i) { WriteCmd(0x80 | (i-32)); WriteCmd(0x88); // 下半屏列地址 for(j0; j16; j) { WriteData(img[i*32 j*2]); WriteData(img[i*32 j*2 1]); } } WriteCmd(0x36); // 开启图形显示 WriteCmd(0x30); // 基本指令集 }5. 动画效果实现技巧让图形动起来其实就是在不同位置重复显示/擦除。我总结出三种动画实现方式整屏刷新最简单但会有闪烁void ShowAnimation(const unsigned char **frames, int count) { for(int i0; icount; i) { ClearScreen(); ShowFullScreenImage(frames[i]); DelayMs(100); // 控制帧率 } }局部刷新更高效的方式只更新变化区域void UpdateArea(int x, int y, int w, int h, const unsigned char *data) { // 设置区域地址 // 仅写入变化部分的数据 }移位动画利用液晶的起始行设置指令实现平滑滚动void ScrollScreen(int lines) { WriteCmd(0x34); // 扩展指令集 WriteCmd(0x03); // 设置起始行 WriteCmd(lines 0x3F); // 64行循环 WriteCmd(0x36); // 开启图形显示 }实际项目中我常用双缓冲技术先在内存中准备好下一帧图像再快速切换显示。这需要额外内存空间但能避免闪烁。对于ST7920可以这样实现unsigned char buffer[1024]; // 128x64/81024字节 void DrawToBuffer() { // 在buffer中绘制图形 } void SwapBuffer() { memcpy(display_data, buffer, 1024); ShowFullScreenImage(display_data); }6. 项目实战电子时钟UI结合上述技术我们做个带动态图标的电子时钟。首先设计界面布局顶部日期星期文字中部大号时间数字图形字体底部温度和小图标关键实现代码// 自定义图形字体 const unsigned char big_num[10][64] { { /* 数字0的图形数据 */ }, { /* 数字1的图形数据 */ }, //... }; // 天气图标 const unsigned char weather_icons[4][128] { { /* 晴天图标 */ }, { /* 雨天图标 */ }, //... }; void UpdateClock() { // 获取时间数据 struct tm time GetRTC(); // 更新数字 DrawBigNumber(10, 20, time.hour/10); DrawBigNumber(30, 20, time.hour%10); //...其他数字 // 更新天气图标 if(temperature 30) { ShowIcon(90, 40, weather_icons[0]); // 晴天 } //...其他条件 }调试时发现频繁刷新会导致显示残影。解决方法是在每次更新前先清除局部区域void ClearArea(int x, int y, int w, int h) { SetGDRAMAddr(x, y); for(int i0; ih; i) { for(int j0; jw/8; j) { WriteData(0x00); WriteData(0x00); } } }7. 性能优化与调试技巧经过实际测试发现几个影响显示速度的瓶颈忙检测等待每次操作前都要检查BF标志位优化前 void WriteData(unsigned char dat) { while(CheckBusy()); // 每次等待 //...写入数据 } 优化后 void WriteMultiData(const unsigned char *dat, int len) { while(CheckBusy()); // 只等待一次 for(int i0; ilen; i) { // 连续写入 } }数据打包传输将多次写入合并// 优化前每次写入都要设置地址 SetAddr(x,y); WriteData(d1); SetAddr(x,y1); WriteData(d2); // 优化后利用自动地址递增 SetAddr(x,y); WriteMultiData(buffer, 16);显示闪烁问题在更新复杂图形时可以先用命令关闭显示完成后再开启void SmoothUpdate() { WriteCmd(0x3E); // 关闭显示 //...更新操作 WriteCmd(0x3F); // 开启显示 }常见问题排查表现象可能原因解决方法显示全白对比度设置不当调整V0引脚电压显示乱码初始化时序错误检查复位时序和指令顺序图形错位取模方向不匹配修改取模软件设置部分区域不更新地址设置错误检查GDRAM分区寻址8. 进阶应用触摸交互界面结合触摸屏我们可以打造交互式界面。以菜单系统为例设计触摸区域映射#define BTN1_AREA {10,30,50,70} #define BTN2_AREA {60,30,100,70}触摸检测处理void CheckTouch() { Point p GetTouchPoint(); if(InArea(p, BTN1_AREA)) { HighlightButton(1); ExecuteCommand(1); } //...其他区域检测 }视觉反馈void HighlightButton(int id) { // 反色显示按钮区域 SetGDRAMAddr(btn[id].x, btn[id].y); for(int i0; ibtn[id].h; i) { for(int j0; jbtn[id].w/8; j) { unsigned char dat ReadData(); WriteData(~dat); // 取反实现反白 } } }这种方案在工业控制面板上非常实用。我做过一个案例通过触摸切换不同的设备状态显示页每页都有对应的示意图和参数表。关键是要做好触摸去抖和界面状态管理。