资讯动态

LCD1602进阶:光标定位、数字显示与局部滚屏实战指南

发布时间:2026/9/15 1:31:16 来源:尧图企业网站定制
做单片机项目这么久LCD1602应该是我用得最多的显示器件。这玩意儿便宜、皮实、资料多但绝大多数人用了一段时间就停在“点亮、显示字符”的水平上。直到某次做个多级菜单的仪表项目我发现自己居然被光标定位、数字刷新、单行滚屏这些看似基础的操作折腾到深夜。这让我意识到LCD1602里的门道其实不少值得单独写一篇出来聊聊。这篇文章不谈抽象理论就聚焦三个高频难题光标定位、显示数字、局部滚屏把原理、代码、坑点一次性讲透。适合已经能让LCD1602亮起来、但想更进一步的朋友。1. 为什么这三招值得单独拿出来讲1.1 LCD1602的“显示地图”与DDRAMLCD1602叫“1602”是因为它一行能显示16个字符、一共两行。但很多初学者没意识到这块屏幕背后有张“隐形地图”叫做DDRAM显示数据存储器。你写入的每一个字符最终都会落在DDRAM的某个地址上然后屏幕再把DDRAM里的内容映射出来。这个映射关系非常固定第一行的DDRAM地址是0x00到0x0F对应屏幕第1到第16列第二行的地址是0x40到0x4F对应屏幕第二行第1到第16列。注意第二行的起始地址不是0x10而是0x40中间隔了48个字节。很多人写代码时想当然地把第二行当成“0x10开始”结果字符全部跑到第一行中间去了这就是不懂DDRAM地址映射的典型下场。搞懂这张地图之后光标定位就豁然开朗了所谓的“光标定位”本质上不是移动一个光标而是告诉LCD1602“接下来要写入的数据请放到DDRAM的哪个地址”。1.2 光标在LCD里到底是什么LCD1602里确实存在一个“光标”概念但和电脑文本编辑器里的光标完全不同。电脑的光标是个闪烁的竖线而LCD1602的光标有两种形态一个是下划线光标在字符位置底部显示一条下划线一个是闪烁块把整个字符位置反转成黑色块。这两种光标由显示控制指令的bit控制你可以让它们显示或消失。这里就有个很多人踩过的坑初始化时如果用了0x0F显示开、光标开、光标闪烁屏幕上会出现一个晃眼的黑色方块而且它还会跟着你写入字符的位置移动非常碍事。所以做实际项目时大多数情况应该用0x0C——只开显示关掉光标。只有在需要手动校准位置、或者做输入框提示时才故意打开光标。理解了DDRAM映射和光标本质后面的操作就都顺理成章了。光标定位、显示数字、局部滚屏这仨功能本质上都在围绕“DDRAM地址”做文章。2. 光标定位从“盲写”到“指哪打哪”2.1 定位指令的底层逻辑光标定位的指令格式是0x80 | 地址。为什么是0x80因为LCD1602指令的最高位为1时代表这是一条“设置DDRAM地址”指令剩下的7位就是你要设置的地址。比如想定位到第一行第1列地址为0x00指令就是0x80 | 0x00 0x80想定位到第一行第5列地址为0x04指令就是0x84。第二行的定位也一样只要地址是0x40或以上即可。比如第二行第1列对应0x40指令是0xC0第二行第4列地址0x43指令是0xC3。很多人背下了0x80是第一行、0xC0是第二行却不知道这个0xC0其实就是0x80 0x40于是遇到“第二行第6列”这种需求时就开始瞎试写出0xC6这样看着对、实际也对的指令但没搞懂原理。搞懂了“基地址偏移”之后任意行列你都能随手算出来。2.2 不同驱动模式下定位的写法我平时写驱动喜欢封装一个LCD_SetCursor函数这样上层逻辑根本不用关心DDRAM地址这些事void LCD_SetCursor(unsigned char row, unsigned char col) { unsigned char addr; if (row 0) addr 0x00 col; else addr 0x40 col; LCD_WriteCmd(0x80 | addr); }参数row取0或1col取0到15。调用时先传row再传col例如LCD_SetCursor(1, 3)表示定位到第二行第4列。这里有个细节我特别提醒一下如果你用的是4位总线模式这个函数逻辑不变但LCD_WriteCmd内部要把指令拆成高低两个字节顺序是“高4位在前、低4位在后”这和8位模式有本质区别别把4位模式的时序套在8位模式上。2.3 光标定位的实战经验定位函数看起来简单实际用起来有几个细节值得注意。第一连续写入字符后DDRAM地址会自动加1前提是初始化时设置了0x06即写入后光标右移这意味着你连续写完一串字符后下一次定位前要记得重新设置地址否则字符会接着上次的位置往下写。第二有些LCD1602模块在写入时存在“地址残留”如果你先定位到某位置写字符然后清屏再定位到另一个位置中间最好加一个短延时给模块时间处理。我实测过清屏指令0x01的占用时间比其他指令长得多官方手册要求等1.5ms以上但很多国产模块实际上要等更久。所以在清屏后加个2到5毫秒延时能省掉不少“偶尔乱码”的烦恼。第三也是最重要的光标定位后如果你不希望用户看到下划线光标或闪烁块记得在初始化或者显示结束时执行LCD_WriteCmd(0x0C)。这个细节在调试阶段无所谓但一旦做成产品就会显得很业余。3. 显示数字字符型液晶最难的一关3.1 数字转ASCII的原理LCD1602是字符型液晶它内部只存了一份字符表CGROM里面是ASCII码和一些日文、希腊字母。你写入一个字节它就显示这个字节对应的字符。问题来了数字和字符是两套编码。数字5的ASCII码是0x35字符5才是0x35而数字5本身在LCD里不直接对应任何可见字符。所以想让LCD显示数字“5”你必须写0x35也就是字符5的ASCII码。最简单的换算就是数字 0因为ASCII码表里数字0到9是连续排列的0的ASCII码是0x301是0x31依此类推。比如数字7的ASCII码就是0x30 7 0x37。这个道理很多教程都讲过但实际项目中你一定会遇到更复杂的情况要显示的往往不是单个数字而是多位数字甚至浮点数、负数、带单位的值。这时候光知道“加0x30”就不够用了。3.2 多位数拆分与显示多位数字的核心问题是拆分。比如你要显示一个变量num 12345就要把它拆成1、2、3、4、5五个数字再分别转成ASCII送入LCD。取个位用num % 10去掉个位用num / 10循环下去就能从低位到高位拆出所有数字。但LCD写入顺序是从高位到低位所以通常先拆到一个临时数组里再逆序写出去。我常用的写法是这样的void LCD_ShowNum(unsigned char row, unsigned char col, unsigned int num) { unsigned char buf[6]; unsigned char i 0; if (num 0) { LCD_SetCursor(row, col); LCD_WriteData(0); return; } while (num 0) { buf[i] num % 10 0; num / 10; } LCD_SetCursor(row, col); while (i 0) { LCD_WriteData(buf[--i]); } }注意我在这里处理了num 0的边界情况否则0会被显示成空字符串。缓冲区大小我写的是6因为unsigned int在51系列单片机上最大是65535最多5位数字加上结束符保险。如果还要显示更大的数据那就得用unsigned long并扩容数组。3.3 数字显示的进阶技巧显示负数怎么办简单先判断符号位是负数就写一个-然后把数值取绝对值再走上面的流程。显示浮点数呢更常用的是固定小数位法比如想显示1位小数的温度值可以先把浮点数乘10转成整数然后显示“整数部分.”和“小数部分”例如25.6度先算256显示时先写256 / 10 25再写.6。这个方法比用sprintf省资源得多在51这种内存紧张的单片机上尤其重要。说到sprintf我要泼盆冷水尽量不要在51上用它。sprintf会引入大体积的格式化代码编译后轻松多占用几KB的Flash还可能拖慢运行速度。做51项目格式化输出自己手写个辅助函数比啥都香。显示数字的另一个细节是“刷新时残留”。如果你在一个位置显示“123”下次显示“45”后面那个“3”的位置可能会残留上次的字符。解决办法就是每次显示前用空格把整个显示区域清一遍或者干脆在写完数字后把后面几个位置补空格。我习惯定义一个最大宽度参数比如固定显示5位不足的在前面补空格这样数字在LCD上就稳定不跳了。4. 局部滚屏别被0x18指令带偏了4.1 为什么直接左移指令会整屏滚动LCD1602有两条指令0x18左移和0x1C右移看名字像是“光标或显示移位”。很多新手想要“让第一行的字滚起来”就直接调用0x18结果发现第一行滚了、第二行也跟着滚整个画面全部跑偏然后怎么调都调不回来。这是因为0x18移动的是整个显示窗口而不是某一行。它做的事情是把屏幕当前显示的8位窗口整体左移一位原本被挤出去的内容就看不到了而且这个移位是显示层面的DDRAM里的数据根本没变。也就是说你把光标定位到第一行、执行0x18屏幕所有内容包括第二行都会向左挪一格这根本不是局部滚屏。那有没有办法用0x18实现局部滚屏理论上如果第二行没有内容或者你不介意第二行也跟着滚那确实可以凑合用。但在实际项目中我们往往需要“第一行滚动显示第二行固定显示菜单或状态”这时候0x18就是个大坑。我最初做滚动菜单时就在这里浪费了两个小时。4.2 局部滚屏最稳的做法DDRAM数据重写正确的局部滚屏思路是不依赖移位指令而是自己动态计算DDRAM内容并且只重写要滚动的那一行。具体做法是维护一个滚动偏移量offset每次都从超长字符串里取一段16个字符的子串写到LCD的某一行。比如要滚动显示字符串HELLO LCD1602 LOCAL SCROLL DEMO...第一行先显示前16个字符延时后offset加1再取第2到第17个字符写回第一行以此类推。因为第二行从来没被写入过所以它永远不会受影响。unsigned char code scrollStr[] HELLO LCD1602 LOCAL SCROLL DEMO...; void LCD_ScrollLine(unsigned char row, unsigned char offset) { unsigned char i; unsigned char len strlen((char*)scrollStr); LCD_SetCursor(row, 0); for (i 0; i 16; i) { LCD_WriteData(scrollStr[(offset i) % len]); } }主循环里每过一段时间调用一次LCD_ScrollLine(0, offset)就行。取模运算保证滚动到尾部后自动跳回开头形成一个无限循环的长条字幕。这个方法看起来“笨”但胜在稳定唯一的问题是刷新时会有轻微闪烁。4.3 进阶平滑滚动与双行独立滚动有人会问这个写法刷新速度一快屏幕就闪得厉害能不能做成像LED点阵屏那样平滑滚动我得说实话LCD1602是字符型液晶字符位置是离散的硬件上就无法做到“逐像素”平滑移动。你最多能加快刷新频率、让字符切换看起来连贯一些但本质还是“逐字符跳动”。想追求平滑滚屏只能换图形点阵屏那是另一个方案了。双行独立滚动呢完全可以。只要写两个滚动函数分别维护两个不同的offset在同一个主循环里轮流刷新两行就行。比如第一行滚动显示时间字符串第二行滚动显示自定义消息。我做过一个简陋的双行字幕时钟效果就是上方一行滚秒数下方一行滚自定义文本互不干扰。另外还有一个细节滚动显示时如果字符串长度小于16取模会导致字符重复或者位置错乱。所以我一般会在取模之前判断一下如果len小于16可以先把字符串填充到16个字符尾部补空格这样滚动效果才正常。这个边界情况很多资料不会写但实际调试时非常容易出现。5. 一套完整的可运行代码5.1 硬件接线与初始化理论说了这么多下面给一套能直接跑的完整代码。硬件平台我用的是最常见的STC89C52RC最小系统板LCD1602采用8位并行接法LCD引脚接单片机引脚RSP2.6RWP2.5ENP2.7D0-D7P0.0-P0.7注意P0口是开漏输出必须外接10kΩ上拉电阻否则数据显示会出现随机乱码。如果你用的是P2口这类准双向口通常可以省略上拉但为了通用我还是建议保留。初始化函数要按LCD1602手册的时序来上电后延时15ms以上连续三次写0x38然后依次写0x08关显示、0x01清屏、0x06写入后地址加1、0x0C开显示、关光标。5.2 完整代码展示下面这段代码是精简但完整的驱动包含了光标定位、数字显示、局部滚屏三个核心功能。#include reg52.h #include string.h #define LCD_DATA P0 sbit LCD_RS P2^6; sbit LCD_RW P2^5; sbit LCD_EN P2^7; void delay_us(unsigned int t) { while (t--); } void delay_ms(unsigned int t) { unsigned int i, j; for (i 0; i t; i) for (j 0; j 120; j); } void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_EN 1; delay_us(50); LCD_EN 0; delay_ms(2); } void LCD_WriteData(unsigned char dat) { LCD_RS 1; LCD_RW 0; LCD_DATA dat; LCD_EN 1; delay_us(50); LCD_EN 0; delay_ms(2); } void LCD_SetCursor(unsigned char row, unsigned char col) { unsigned char addr; if (row 0) addr 0x00 col; else addr 0x40 col; LCD_WriteCmd(0x80 | addr); } void LCD_Init(void) { delay_ms(15); LCD_WriteCmd(0x38); delay_ms(5); LCD_WriteCmd(0x38); delay_ms(5); LCD_WriteCmd(0x38); LCD_WriteCmd(0x08); LCD_WriteCmd(0x01); LCD_WriteCmd(0x06); LCD_WriteCmd(0x0C); } void LCD_ShowString(unsigned char row, unsigned char col, unsigned char *str) { LCD_SetCursor(row, col); while (*str) { LCD_WriteData(*str); } } void LCD_ShowNum(unsigned char row, unsigned char col, unsigned int num) { unsigned char buf[6]; unsigned char i 0; if (num 0) { LCD_SetCursor(row, col); LCD_WriteData(0); return; } while (num 0) { buf[i] num % 10 0; num / 10; } LCD_SetCursor(row, col); while (i 0) { LCD_WriteData(buf[--i]); } } unsigned char code scrollStr[] HELLO LCD1602 LOCAL SCROLL DEMO...; void LCD_ScrollLine(unsigned char row, unsigned char offset) { unsigned char i; unsigned char len strlen((char*)scrollStr); LCD_SetCursor(row, 0); for (i 0; i 16; i) { LCD_WriteData(scrollStr[(offset i) % len]); } } void main(void) { unsigned char offset 0; unsigned int counter 0; LCD_Init(); LCD_ShowString(1, 0, Count:); while (1) { LCD_ShowNum(1, 6, counter); counter; if (counter 999) counter 0; LCD_ScrollLine(0, offset); offset; delay_ms(300); } }5.3 代码运行效果这段代码的实际效果是第一行缓慢滚动显示长字符串第二行固定显示Count: 0并且这个计数器每300毫秒递增一次。你把它烧录进单片机后应该能直接看到两个区域互不干扰地工作上边在滚下边在跳数字。这正是“局部滚屏”和“动态数字刷新”的典型组合场景。如果想调整滚动速度改delay_ms(300)里的300即可。从我的经验看滚动字幕间隔200到400毫秒比较舒适太快会闪太慢又显得拖沓。末端字符滚出屏幕后因为取了模会重新从开头出现形成一个无限循环的跑马灯效果。6. 常见问题与排查技巧实录6.1 常见故障速查表实操中总会遇到各种奇奇怪怪的现象我整理了一个速查表按“现象—原因—解决”的顺序排列都是我自己踩过的坑现象原因解决办法上电后第一行出现一排黑块初始化时序没有按规范执行或数据线接触不良检查接线初始化前延长上电延时逐条检查命令间隔第一行正常第二行显示在第一行中间第二行定位地址写成了0x10第二行起始地址必须是0x40写命令时用0xC0数字显示成“: ; ”之类的乱码直接把数字写给了LCD没有加0转ASCII码num 0滚动时两行都移动使用了0x18整屏移位指令改用DDRAM数据重写方案只刷新目标行显示内容偶尔闪烁刷新频率过高或P0口没有上拉电阻延时长一点检查上拉电阻和总线电平清屏后偶尔还残留旧字符清屏指令后没给足延时0x01清屏后至少延时2ms以上6.2 几个容易被忽略的细节第一LCD_WriteData和LCD_WriteCmd的延时不要省。有些模块对时序不敏感延时短了也能跑但换块屏就翻车。我一般写命令延时2ms、写数据延时2ms顺手可靠。如果追求速度可以用读忙标志位的方式替代固定延时但代码复杂度会上升对多数项目来说没必要。第二字符串指针用unsigned char *类型避免在某些编译器下出现符号扩展问题。虽然多数51编译器对char默认是无符号的但写成unsigned char能防止跨平台移植时出幺蛾子。字符串内容如果不会改变建议加code关键字存到Flash里省下宝贵的RAM空间。第三滚动字符串的长度如果小于16局部滚屏会出现“空窗”。比如scrollStr只有6个字符取模后不到16个屏幕右侧会露出空白。保险做法是确保字符串长度大于16或者手动补空格到16个字符以上。第四很多人找LCD1602驱动代码喜欢去网上东拼西凑其实核心驱动就这么几行。与其花时间找“万能驱动”不如把初始化、写命令、写数据这三个函数吃透遇到任何问题都能自己改。驱动代码的底层逻辑都一样区别只在于引脚定义和总线模式理解之后你自己就是下载站。我个人的经验是玩LCD1602最重要的不是背指令而是把DDRAM地址映射、字符编码、指令时序这三件事想明白。光标定位是改地址显示数字是改编码局部滚屏是改数据源三个问题背后其实是同一套逻辑。下次不管换什么型号的字符型液晶这套思路照样能用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价