LCD12864汉字滚动显示详解:从硬件选型到平滑滚动算法

📅 发布时间:2026/9/2 8:32:19
LCD12864汉字滚动显示详解:从硬件选型到平滑滚动算法
简介基于C51单片机的LCD滚动显示汉字工程包面向电子工程、嵌入式系统初学者及单片机课程设计人群重点演示通过I/O口驱动点阵LCD、存储并读取汉字点阵字模以及利用显示缓冲区实现向左滚动显示的核心思路涵盖单片机原理、C51编程、LCD显示技术与常用开发工具链。压缩包共18个文件、约57KB包含Keil工程源码chengxu.c、STARTUP.A51、编译生成的hex文件、object/lst/m51等中间文件以及Proteus仿真电路DSN和工程备份文件可完整还原开发环境并对照学习程序与硬件电路。已有2283人学习该资源。通过该工程可掌握C51操作LCD的初始化流程、汉字字模在程序存储器的组织方式、改变显示起始地址实现滚动的方法以及延时控制滚动速度和减少闪烁的优化思路对课程设计、毕业设计或竞赛准备均有直接参考价值。1. 为什么LCD滚动显示汉字能把人卡一整天先说我自己的经历。第一次做LCD显示用的是LCD1602兴冲冲接好线、调好对比度跑了个Hello World一切正常。然后老师/老板/课程设计的要求加了一句让它滚动显示汉字。我当时想这有什么难的左移呗。结果一查资料才发现自己天真了1602内置字库根本没有汉字。它只能显示ASCII字符和日文假名你想让它显示单片机三个字它连一个汉字都拼不出来。这就是这个项目最坑的地方——很多人卡在第一步不是因为不会写滚动而是因为选错了硬件。LCD1602和LCD12864看起来长得差不多引脚也类似但12864才有真正显示汉字的能力。之后我把项目换成了带中文字库的LCD12864控制器多为ST7920又踩了取模、滚动重绘、残影、闪烁一堆坑才算是把滚动显示汉字这件事彻底吃透。这篇文章就把整个方案从头到尾梳理一遍。既讲清楚硬件选型和原理也给出完整的51单片机8051内核驱动代码和滚动算法最后总结几个典型的调试故障。适合正在做单片机课设、蓝桥杯备赛或者刚入门想搞懂LCD底层显示原理的读者。2. 解码滚动显示的本质不是移动文字而是重画画面2.1 LCD屏幕上到底发生了什么要想让汉字滚动起来首先要理解你的屏幕看见的是什么。LCD12864的分辨率是128×64像素意味着屏幕上有128列、64行的点阵。你要显示的汉字不是文字而是一张张由点构成的小图片。一个标准汉字比如说16×16点阵占16列×16行。12864屏幕上这样大小的汉字一屏最多显示8列×4行如果按16×16为单元格算横向128/168个纵向64/164个。带字库的12864可以直接发送汉字的内码控制器内部自动把字模画上去不带字库的12864则必须由你自己准备字模数据告诉它每个点亮不亮。关键来了LCD控制器没有滚动画面这种硬件功能多数廉价模块不支持整屏硬件滚动ST7920虽然有滚动寄存器但在很多兼容模块上并不可靠。所以滚动显示在嵌入式里几乎一律通过软件实现——也就是按一定的频率重新把整屏数据全部画一遍每次画的起始位置改变一点点。这事儿就好比翻页动画书。每页单独看是静止的快速翻页就产生了字在动的错觉。MCU要做的就是高效地翻页。2.2 滚动的两种思路左移一列还是左移一字滚动方案一般分两种很多人一开始没想清楚就动手结果做出来效果很生硬。第一种是整字跳变滚动也叫步进滚动。每次刷新整个字符串向左移动一个汉字的宽度16列。实现简单代码量小但视觉效果是跳而不是滚观感比较生硬。第二种是逐列平滑滚动每次刷新只向左移动一列。视觉效果顺滑很多看起来是真正的滚动但刷新的开销是前者的16倍对时序控制要求更高。这也是我推荐的目标效果。想清楚这两种方案的区别再往下写代码就有方向了。逐列滚动的核心是处理好当前窗口的坐标映射后面第4章我会展开讲。3. 硬件选型与接线别让对比度电位器坑了你3.1 LCD1602、LCD12864带字库、不带字库到底选谁型号分辨率内置汉字库显示汉字方式典型控制器LCD160216×2字符无5×8点阵只能造8个自定义字符显示不了完整汉字HD44780LCD12864带字库128×64像素有GB2312直接发送汉字内码ST7920LCD12864不带字库128×64像素无需要自己存字模数据按点阵画KS0108、ST7565等结论先给出来做滚动显示汉字这个项目首选带中文字库的12864。原因很简单带字库的方案你只需要告诉它在第2行第0列显示单字控制器就自动把汉字点阵画好不带字库的方案你得自己准备每一个汉字的64字节字模数据工程量翻好几倍。那LCD1602还有没有救有但很屈辱——它的CGRAM字符生成RAM只有8个5×8空格你可以自己定义8个字符的图案用2×2个字符拼一个伪汉字但字形会严重变形而且最多同时显示一个汉字。做滚动显示汉字这种需求还是别难为1602了。3.2 引脚接线和三个容易翻车的硬件细节这里以51单片机和带字库LCD12864为例标准接法如下不同的模块丝印引脚名略有差异接线前务必查你手里模块的 datasheetVSS 接电源负极GNDVDD 接电源正极5VVO 接对比度调节电位器中间抽头两脚分别接VDD和GNDRS有些模块标CS接单片机P2.5控制数据/指令选择RW部分模块标SID接P2.6控制读/写E部分模块标SCLK接P2.7使能时钟DB0~DB7 接P0口8位模式或只用高4位接4位模式PSB 接VDD高电平选择并行模式若接GND则是串行模式LEDA / LEDK 背光正极和负极串个几十欧电阻再接电源翻车点一VO悬空。很多初学者不接电位器屏幕出来一片黑块还以为是初始化代码有问题其实是对比度太高/太低。接一个10K电位器调到清晰显示就好。翻车点二PSB模式选错。有的模块默认跳线是串行模式你按照并行时序写代码怎么折腾都不出字。检查这个引脚比检查代码优先级高。翻车点三P0口是开漏输出驱动LCD时必须外接上拉电阻典型4.7K~10K到VDD。不接上拉P0输出高电平能力很弱液晶显示会出现某些段亮起来很暗甚至不亮。4. 字模与取模乱码九成出在这一步4.1 16×16点阵为什么是汉字显示的最小单位在计算机里一个汉字至少需要16×16点阵才能清晰辨认。这个尺寸不是随便定的它源于汉字字形的最小可识别要求。16×16256个点每个点用一个二进制位表示1点亮0熄灭所以一个汉字需要256位32字节的数据。如果是32×32点阵就是128字节显示更漂亮但数据量同步翻四倍。对于单片机RAM和FLASH都比较紧张的场景16×16是性价比最高的选择。取模的概念就是把汉字图形按照从左到右、从上到下逐行逐列扫描记录每个点是否点亮生成一组十六进制数据。市面上取模软件很多比如字模提取软件、PCtoLCD2002等等但用软件取模前必须先搞清楚取模方式和字节顺序这两个参数搞错了显示必乱。4.2 取模方向纵向取模还是横向取模这是本项目最大的坑。同一个汉字按照不同取模方式得到的hex数据完全不同而LCD控制器只认识其中一种。以最常见的16×16点阵为例无字库12864如KS0108控制器通常采用列行式/逐列式显示控制器按列扫描先写第0列的16个点2字节再写第1列的16个点……一共16列共32字节。所以取模时要设置成纵向取模、字节倒序或者纵向取模、正向取模——不同软件叫法不一样。更准确的描述是列的每8个点形成一个字节高位在上还是在下必须实测确认。这直接决定汉字会不会上下颠倒、左右翻转。提示如果你用带字库的12864ST7920不需要自己取模直接发汉字GB2312内码即可。取模这节主要针对无字库屏幕。但无论哪种屏幕理解字模原理都能帮你少走弯路。4.3 数据组织一张表管理所有需要的汉字假设你要滚动显示单片机开发板系统欢迎您这串文字先把需要的汉字做成字模数组。每个汉字32字节按顺序编号// 假设已经通过取模软件生成了16x16点阵数据 // u8为unsigned char u8 code hanzi_table[][32] { {0x00,0x10,0x18,0x1C,0x18,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00, 0x40,0x40,0x40,0x40,0x40,0x40,0x60,0x50,0x48,0x44,0x42,0x41,0x40,0x40,0x40,0x00}, // 单示例数据实际需用取模软件生成 // 其他汉字按顺序排列... };一个很实用的技巧把字符串里的字做一个字形索引表程序里用字符查表定位字模位置。如果字库是你自己取的就必须保证取模顺序和索引逻辑一致避免出现显示的汉字和实际想要的不匹配这种尴尬问题。4.4 高位在前还是低位在前一个bit决定生死整理字模时还有一个容易忽略的细节每个字节内部的bit与屏幕像素的对应关系。假设一个字节表示某一列从上到下的8个像素通常规定bit7对应最上面的像素bit0对应最下面的像素。很多取模软件默认就是这个顺序。但如果你拿到的字模数据是反的就会出现汉字上下颠倒。这个时候不用重新取模写一个字节反转函数把每个字节的bit顺序反一下即可u8 reverse_byte(u8 byte) { byte (byte 0xF0) 4 | (byte 0x0F) 4; byte (byte 0xCC) 2 | (byte 0x33) 2; byte (byte 0xAA) 1 | (byte 0x55) 1; return byte; }在写入字模前统一处理比手动改数据靠谱得多。5. 滚动算法实战从跳字到平滑滚动5.1 场景设定和数据结构设计我现在以无字库12864 自取模汉字为例讲解滚动的完整实现因为这种方式最透明你能看到每个像素点是如何被移动的。如果你用带字库屏只需要把画汉字替换成显示汉字内码的函数滚动逻辑完全一样。先说数据结构。假设滚动区域放在屏幕左上角显示窗口宽128列、高16行正好容纳一行16×16汉字。滚动窗口里要显示的字符串由N个汉字组成每个汉字16列宽。我们需要三个关键变量#define HANZI_WIDTH 16 #define HANZI_HEIGHT 16 #define SCREEN_WIDTH 128 u8 index_start 0; // 当前窗口最左侧显示的是字符串中第几个汉字 u8 pixel_offset 0; // 已向左平移了多少列0~15之间循环 u8 timer_tick 0; // 定时器累计的刷新计数滚动逻辑每经过固定时间间隔pixel_offset当pixel_offset达到16时说明已经完整移出了一个汉字此时index_start、pixel_offset归零。屏幕上某个位置x0~127对应的汉字序号和汉字内偏移计算如下void get_source_info(u8 x, u8 *char_index, u8 *offset_in_char) { u16 total_offset index_start * HANZI_WIDTH pixel_offset x; *char_index total_offset / HANZI_WIDTH; *offset_in_char total_offset % HANZI_WIDTH; }这就把屏幕坐标 → 字符串坐标的映射打通了。5.2 逐列滚动的核心绘制函数以无字库12864为例一次刷新要做的其实是先清屏然后对屏幕上的每一列x从上到下逐点画出来。void draw_scrolling_text(void) { u8 x, y, char_idx, offset_in_char; u8 *font NULL; u8 byte, bit_mask; clear_screen(); // 全屏清0避免残影 for (x 0; x 128; x) { get_source_info(x, char_idx, offset_in_char); if (char_idx text_length) { // 已滚出字符串末尾可在这里决定是否回到开头循环滚动 font NULL; } else { font hanzi_table[text_index[char_idx]]; } for (y 0; y 16; y) { if (font NULL) byte 0x00; else { // 纵向取模列优先每8个点一个字节 if (y 8) { byte font[offset_in_char * 2]; // 列上半部分字节 bit_mask 0x80 y; } else { byte font[offset_in_char * 2 1]; // 列下半部分字节 bit_mask 0x80 (y - 8); } } if (byte bit_mask) { lcd_draw_pixel(x, y, 1); } } } lcd_refresh_screen(); // 把显存同步到LCD }这段代码的核心思想是用坐标计算代替物理移动数据。每一帧都根据index_start和pixel_offset重新计算出所有像素的归属然后一次性重绘。虽然看起来每次都在重画但在8位MCU上只要频率控制得当完全能跑出流畅的效果。5.3 用定时器中断驱动刷新不用延时函数很多人写滚动第一版都是这么干的主循环里delay(100ms)然后刷新一次。结果发现屏幕上文字在抖或者闪原因有两个。第一延时函数浪费CPU时间期间如果有按键扫描、串口接收等任务全部被卡死。第二延时期间屏幕保持上一次画面不变刷新频率不稳人眼就会感知到闪烁。正确做法是用定时器中断产生一个固定的时间基准。以STC89C52/STC15这类51单片机为例用定时器0产生2ms中断累加变量达到设定的刷新周期比如50ms就置一个refresh_flag主循环检测到这个标志才执行draw_scrolling_text()。void timer0_isr() interrupt 1 { TH0 0xFC; // 重载初值假设12MHz晶振、12T模式2ms定时 TL0 0x18; tick_count; if (tick_count 25) { // 25 * 2ms 50ms刷新一次 tick_count 0; refresh_flag 1; } } void main() { init_lcd(); init_timer0(); EA 1; while (1) { if (refresh_flag) { refresh_flag 0; pixel_offset; if (pixel_offset HANZI_WIDTH) { pixel_offset 0; index_start; if (index_start text_length) { index_start 0; // 循环滚动 } } draw_scrolling_text(); } // 其他任务按键扫描、串口处理等可以放在这里 } }刷新周期不要设得太短50ms是比较稳妥的起步值。太快会感觉文字在跳太慢会明显一顿一顿。如果你用的是12MHz晶振且机器周期为1T比如STC15系列定时器初值需要按实际时钟重新计算不能照抄。5.4 显存与刷新128×64位图怎么组织上面代码里出现了lcd_draw_pixel()和lcd_refresh_screen()这里解释一下它们的实现思路。在MCU上建议维护一块显存显存就是屏幕内容的镜像因为直接向LCD逐点写命令操作数量巨大且容易闪烁。无字库12864通常按16行分页每页64字节。所以显存可以这样定义u8 lcd_buffer[64][8]; // 64列不对要稍微想清楚 // 实际上128x64更适合按页组织 // 64行 8页每页8行x方向128列 u8 lcd_buffer[8][128]; // 8页每页128字节lcd_draw_pixel(x, y, on)负责把像素对应的位写入lcd_buffer[y/8][x]。lcd_refresh_screen()则把8×128个字节一次性按LCD控制器的页地址和列地址顺序写出去。这两个函数构成了所有显示功能的底层地基清晰易懂。带字库的12864则不需要这种低位操作直接调用显示函数写内码就行显存方案可以简化甚至省略。6. 完整例程51单片机 LCD12864 滚动显示汉字为了让你能直接跑通我给出一个完整的简化工程。这里我选用不带字库的LCD12864来演示因为这样你能看到字模、滚动算法的全貌如果你手里是带字库屏替换底层写数据函数即可。6.1 硬件连接LCD引脚单片机引脚RSP2.5RWP2.6EP2.7DB0~DB7P0.0~P0.7PSBVDD并行模式V010K电位器中点LEDA5V串100R电阻LEDKGND6.2 底层驱动和主程序#include reg52.h #include string.h #define u8 unsigned char #define u16 unsigned int sbit RS P2^5; sbit RW P2^6; sbit E P2^7; u8 code text_index[] {0,1,2,3,4,5}; // 需要显示的汉字在字模表中的索引 u8 code text_length 6; // 共6个汉字 // 检查忙标志 void lcd_check_busy(void) { u8 temp; RS 0; RW 1; do { P0 0xFF; E 1; temp P0 0x80; E 0; } while(temp ! 0); } // 写指令 void lcd_write_cmd(u8 cmd) { lcd_check_busy(); RS 0; RW 0; P0 cmd; E 1; E 0; } // 写数据 void lcd_write_data(u8 dat) { lcd_check_busy(); RS 1; RW 0; P0 dat; E 1; E 0; } // 初始化KS0108类型12864 void lcd_init(void) { lcd_write_cmd(0x3E); // 关显示 lcd_write_cmd(0x40); // 设置起始行0 lcd_write_cmd(0xB8); // 设置页地址0 lcd_write_cmd(0xC0); // 设置列地址0 lcd_write_cmd(0x3F); // 开显示 } // 在指定页、列显示一点简单绘图函数 void lcd_clear(void) { u8 page, col; for (page 0; page 8; page) { lcd_write_cmd(0xB8 | page); lcd_write_cmd(0x40); for (col 0; col 64; col) { lcd_write_data(0x00); lcd_write_data(0x00); } } } // 假设已经实现了lcd_draw_pixel和lcd_refresh_screen // 底层驱动细节较多此处省略完整代码可参考后续说明 void main(void) { lcd_init(); lcd_clear(); while (1) { // 定时器中断中维护index_start和pixel_offset // 主循环检测refresh_flag调用draw_scrolling_text() } }注意KS0108类屏幕左右各64列写数据时要先切换左右半屏的片选信号CS1/CS2这里为了简洁只写了核心逻辑实际工程中需要补充左右屏选择。ST7920带字库屏则不需要拆左右。6.3 运行之后你会看到什么上电后第一帧显示字符串的最左侧6个字每隔50ms整个画面向左移动一列直到最左侧的汉字完整移出屏幕接着所有文字继续左移最后index_start回到0实现循环滚动。这种现象用肉眼看起来就是一行汉字像跑马灯一样平滑连续地从右往左滚动。7. 调试实录那些看着没问题但就是显示不对的坑7.1 汉字上下两半错位如果你显示出来的汉字上下两部分不对称比如上半部分正常、下半部分跑到同行的左/右侧去了基本可以断定是字模字节顺序和LCD页扫描顺序不一致。无字库12864的页地址从0到7每页8行。一个16×16汉字占2页。如果取模软件生成的顺序是先第一页16列、再第二页16列而你的代码是先画出第0列的上下两个字节、再画第1列就会错位。解决办法很直接在draw_scrolling_text里对每一个x先写上半字节再写下半字节保持和取模格式一致即可。7.2 滚动时文字出现残影或拖尾残影多半是因为刷新频率低于人眼融合频率上一帧图像还没消退下一帧就叠上去了。先说结论用定时器固定刷新周期 每次刷新前先全部清屏。很多人为了省时间只在滑动的新区域补画新列其余部分不清觉得这样效率更高——实际效果往往一团糟。LCD是行列扫描器件不清屏就直接覆盖会导致旧图像残留。正确做法就是像第5.2节代码那样clear_screen() 全部重绘每次全量刷新简单可靠。7.3 写字模数组时Keil报内存溢出51单片机的片内RAM通常只有128字节或256字节字模数组如果定义成普通全局数组编译时很容易报内存溢出。解决办法是加code关键字把数据放到程序存储器Flash中比如u8 code hanzi_table[][32] { ... };这样数组就不会占用RAM了。同理字符串索引表text_index[]也加code。这是51开发最基础但也最常见的优化动作。7.4 带字库屏发汉字显示出来是乱码带字库12864ST7920本身能显示中文但前提是你写入的是GB2312编码的汉字内码。很多人在Keil工程里把源码文件编码设置成了UTF-8或者编译器的字符集选项不对导致中文字符串在编译后被转成了UTF-8字节序列ST7920不认识自然显示乱码。解决办法确认源码文件编码为GB2312或GBK并且在写显示函数时对每个汉字发送两个字节的内码。不要在源文件里写那种中英混排的长字符串然后直接整串发送建议先把字符串拆成单个汉字数组逐个发送。例如u8 code str_welcome[] 欢迎使用; // 确保源码编码是GB2312 void lcd_show_chinese(u8 x, u8 y, u8 *str) { while (*str) { if (*str 0x80) { lcd_set_pos(x, y); lcd_write_data(*str); // 汉字高字节 lcd_write_data(*str); // 汉字低字节 x 16; } else { // ASCII字符处理 str; } } }7.5 滚动方向反了怎么办如果你希望文字从右往左滚但现在是从左往右滚通常只需要改pixel_offset的增减方向或者把坐标映射函数中的号互换为-号即可。不要重新写一遍驱动这个小坑调整一行代码就能解决。8. 我个人的一点实操体会这个项目做完之后我对LCD显示的理解比之前只抄教程的时候深了一个层次。核心收获有两点一是学会了站在坐标映射的角度看所有显示需求不管是滚动、翻页还是局部刷新本质都是在回答屏幕上的这个像素应该来自内容的哪个位置。二是明白了用定时器中断管理刷新节奏的重要性这个思路在后续做多任务系统、按键防抖、状态机时同样适用。再分享一个小技巧如果你做的是答辩/演示类项目滚动速度可以适当放慢到150ms刷新一次效果更稳重如果是产品交互界面就保持在30~50ms观感更流畅。参数可调不用死记硬背关键是理解帧率这个维度的存在。这个项目后续也非常容易扩展比如把滚动文字改成语音播报把单行滚动改成多行上下滚动或者加上按键切换菜单。只要把底层坐标映射和刷新框架写稳往上加功能都不难。本文还有配套的精品资源点击获取