ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LCD1602实用技巧:光标定位、数字显示与局部滚屏实现

LCD1602实用技巧:光标定位、数字显示与局部滚屏实现 LCD1602这块几十块钱就能买到的老屏幕在工控设备、仪器仪表、课设作品里出现频率高得吓人。大部分人到手就是照着例程抄一遍驱动显示个“Hello World”就完事了。但实际做项目时你会发现真正遇到的往往不是“点亮屏幕”而是“怎么把数字显示成我想要的样子”“怎么让光标停在指定位置闪烁”“怎么让某一行内容滚动起来而其他行保持不动”。这篇文章就把我这些年折腾LCD1602总结下来的几个实用技巧摊开讲光标定位、数字显示、局部滚屏。用STC89C52RC这类51单片机搭配LCD1602是学校课设和DIY最常见的组合网上问“哪里可以下载LCD1602驱动代码”的人也特别多所以我会把底层原理和可直接抄的代码一并给出适合正在做课设、准备电子设计竞赛或者纯粹想把这颗经典屏玩明白的朋友。1. 显示原理与底层驱动所有“骚操作”的地基1.1 为什么建议自己写驱动而不是到处找代码网上关于LCD1602的驱动代码一搜一大把但直接复制粘贴翻车的概率不低。原因很简单LCD1602用的是HD44780控制器通信方式是8位或4位并行需要控制RS、RW、E三个控制引脚加8根数据线。不同开发板、不同最小系统板对引脚的分配并不完全一致网上的代码往往写着“P0口接数据、P2.0接RS”但你的板子可能把数据线接到P1口了不改引脚定义屏幕就是白屏或者乱码。另一个问题是时序。LCD1602对时序的要求虽然不是苛刻到必须用逻辑分析仪但上电等待时间、使能脉冲宽度这些参数如果不对屏幕容易出现“第一行正常第二行乱码”“字符显示一半”之类的怪现象。与其到处找驱动不如花二十分钟自己写一遍既能根据自己硬件改引脚又能把HD44780的指令集顺手搞明白——后面你要做光标定位、滚屏这些操作本质上都是在跟指令集打交道底层不熟上层全是坑。1.2 HD44780指令集里最常用的几个命令HD44780的指令集不算复杂常用的加上功能设置、清屏、地址设置等总共也就十几个。写驱动的时候核心其实就是往指令寄存器里塞命令、往数据寄存器里塞字符具体流程是拉低RS指令模式、拉低RW写模式然后给E引脚一个下降沿把指令锁存进去。拉高RS数据模式再给E引脚一个下降沿把字符数据写入DDRAM。常用指令我整理成了一张表背下来基本就够用了指令二进制码DB7~DB0作用清屏0x01清空DDRAM光标归位归位0x02DDRAM地址归零光标回到左上角不清内容输入模式0x06每写完一个字符地址自动加1屏幕不移动显示开关0x0C开显示、关光标、不闪烁显示开关0x0E开显示、开下划线光标、不闪烁显示开关0x0F开显示、开下划线光标、光标闪烁功能设置0x388位数据、2行显示、5×7点阵字符设置DDRAM地址0x80 | 地址光标移动到指定DDRAM地址写数据无固定值把字符写入当前DDRAM地址地址自动加1LCD1602的DDRAM地址映射很关键。第一行从0x00到0x0F对应屏幕上的第1到第16个字符位置第二行从0x40到0x4F对应第二行第1到第16个位置。所以让光标跑到第二行第1列的命令就是0x80 | 0x40也就是0xC0。1.3 一套干净的基础驱动代码我用STC89C52RC为例数据口用P1控制引脚用P2.0、P2.1、P2.2。这样省掉了P0口需要外加上拉电阻的麻烦。代码里我做了宏定义改引脚时只需要改这几行。#include reg52.h #define LCD_DATA P1 sbit LCD_RS P2^0; sbit LCD_RW P2^1; sbit LCD_E P2^2; void Lcd_Delay(unsigned int t) { while(t--); } void Lcd_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_DATA cmd; Lcd_Delay(100); LCD_E 1; Lcd_Delay(100); LCD_E 0; } void Lcd_WriteData(unsigned char dat) { LCD_RS 1; LCD_RW 0; LCD_DATA dat; Lcd_Delay(100); LCD_E 1; Lcd_Delay(100); LCD_E 0; } void Lcd_Init(void) { Lcd_Delay(15000); // 上电等待让LCD内部电路稳定 Lcd_WriteCmd(0x38); Lcd_WriteCmd(0x0C); Lcd_WriteCmd(0x06); Lcd_WriteCmd(0x01); } 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_ShowString(unsigned char row, unsigned char col, unsigned char *str) { Lcd_SetCursor(row, col); while(*str ! \0) { Lcd_WriteData(*str); str; } }这套代码里没有做忙检测而是用固定延时代替。实际用下来对LCD1602这种低速外设固定延时比忙检测更快也更不容易出问题——尤其是很多国产LCD1602模块在读忙标志的时候DB7引脚在总线释放前会出现不确定状态处理不好反而容易卡死。初始化时的Lcd_Delay(15000)一定不能省很多屏幕白屏就是上电时序没等够。2. 光标定位的“骚操作”从固定显示到动态交互2.1 0x80地址设置命令背后的逻辑HD44780的0x80 | 地址这条指令额外有意思的地方在于DB7位为1表示这是一条“设置DDRAM地址”指令剩下的DB6~DB0才是真正的7位地址。所以很多初学者会疑惑为什么第一行第0列是0x80而不是0x00原因就是DB7必须置1让控制器识别出“这是地址设置”。理解这个逻辑后光标定位就变成了一道简单的加法题。Lcd_SetCursor(0, 5)实际发送的命令是0x80 | 0x05 0x85。Lcd_SetCursor(1, 5)发送的是0x80 | 0x40 | 0x05 0xC5。运算本身不复杂但容易在计算第二行地址时漏掉0x40或者把行列参数搞反。我的习惯是把行列和DDRAM地址的对应关系直接写成注释防止过段时间自己都忘了。2.2 实用案例用光标做密码输入框和菜单高亮光标的两个实用方向一个是“定位后写入”另一个是“让光标本身参与交互”。先看“定位后写入”典型场景是密码输入框。假设屏幕第一行显示“PASSWORD:”第二行需要输入6位密码。需求是每次只显示一个*而且输入框的位置要固定。实现思路是把第二行DDRAM地址先清成空格然后每次按键后Lcd_SetCursor(1, 输入位数)再写入*。这里光标定位的价值在于不需要整屏重写只需要在指定坐标写数据屏幕其他区域不会闪烁。菜单高亮是另一个常见需求。开机时菜单项显示在第二行当前选中项用下划线光标标识。用0x0E打开下划线光标然后Lcd_SetCursor把光标移动到选中项下方就能做到“光标跟着选择走”。配合独立按键每按一次下移就重新定位一次光标一个简单的交互菜单就出来了。这个操作比用反白显示简单而且不需要额外画图形符号。下面是我在课设里用过的一段代码片段作用是在两行分别定位并显示两个变量的内容同时让第二行的下划线光标停在指定位置提示用户操作void Show_Status(unsigned int value1, unsigned int value2) { char buf[6]; // 第一行第3列显示温度值 Lcd_SetCursor(0, 3); sprintf(buf, %3d, value1); // 右对齐占3位 Lcd_ShowString(0, 3, buf); // 第二行第5列显示湿度值 Lcd_SetCursor(1, 5); sprintf(buf, %3d, value2); Lcd_ShowString(1, 5, buf); // 第二行第12列显示下划线光标提示用户此处可操作 Lcd_WriteCmd(0x0E); Lcd_SetCursor(1, 12); }2.3 光标模式切换的几个坑0x0C、0x0D、0x0E、0x0F这四个命令看着像实际效果差异很大0x0C显示开光标关不闪烁。0x0D显示开光标关当前光标位置的字符闪烁注意不是光标方块在闪是字符本身在闪。0x0E显示开下划线光标开不闪烁。0x0F显示开下划线光标开光标闪烁。0x0D很容易被忽略但它有一个妙用想让某个数字特别醒目时把光标停在那位数字上用0x0D让这个字符整体闪烁效果类似“高亮提醒”。不过要提醒一句字符闪烁是整屏光标所在位置在闪不是单独对某个字符做动画如果屏上有多个地方都需要闪烁提醒只能靠切换光标位置来实现做不到多区域同时闪。另外光标定位本身不会清掉原有字符。很多人以为Lcd_SetCursor会把目标位置的字符清空其实它只是改变DDRAM地址原字符还在那里。想“清除”某个位置的内容要么写入空格要么使用清屏指令。这个问题在滚屏场景里尤其明显后面会专门讲到。3. 显示数字的“骚操作”从数码管思维跳出来3.1 为什么LCD1602显示数字要转换字符很多从数码管转过来的人一开始都想直接把数字变量塞给Lcd_WriteData结果屏幕上出现一堆乱码。原因在于LCD1602的数据口接收的是ASCII码不是数值本身。数码管显示0到9是查段码表LCD1602做的是字符码映射。数字字符0对应的ASCII码是0x30也就是十进制的48所以要让屏幕显示“0”数据线上要送0x30。所以显示一位数字的最基础写法是unsigned char num 5; Lcd_WriteData(0 num); // 输出字符5这里有另一个常见误区有人写成Lcd_WriteData(num 0x30)效果也一样但用一个0字符常量表达更直观也省去记0x30这个魔数。对字符串来说更省事的方案是用sprintf把整数格式化输出像前面代码里写的那样。STC89C52RC内部有1KB RAM和8KB Flash调用sprintf虽然会占用2KB左右的ROM空间但换来的开发效率提升是值得的课设里完全够用。如果你是在资源很紧的小芯片上做开发那就自己写一个简单的整数转字符串函数代码量也不大。3.2 多位数、补零、负数和浮点数的处理策略显示多位数字核心问题是“怎么对齐”。LCD1602一共16列如果数字一会儿是一位数一会儿是三位数屏幕上的内容会左右漂移看着非常难受。解决办法是固定位数显示我用%3d这种格式控制符让数字右对齐占3位不够的前面补空格。同理想补零显示就用%03d这在做秒表倒计时时特别有用显示“07”而不是“7”。负数显示不能直接对数字字符做运算需要判符号位然后决定是否先输出一个-。浮点数则更麻烦一点LCD1602没有直接显示浮点数的指令一般做法是分离整数部分和小数部分void Show_Float(float val, unsigned char row, unsigned char col) { int int_part (int)val; int dec_part (int)((val - int_part) * 100); if(val 0) { Lcd_SetCursor(row, col); Lcd_WriteData(-); val -val; } // 重新计算正数部分 int_part (int)val; dec_part (int)((val - int_part) * 100); char buf[8]; sprintf(buf, %d.%02d, int_part, dec_part); Lcd_ShowString(row, col (val 0 ? 1 : 0), buf); }这里有个平时不太注意的细节判断完负数后要重新计算整数部分和小数部分因为取绝对值后int_part和dec_part会变化。我一开始没注意导致负数显示成“-3.-50”这种离谱结果排查了半天才发现是用了旧值。3.3 “数字单位”和“数字状态”的组合显示技巧做仪器仪表类项目屏幕上的数字往往跟着单位或状态字符。比如电压显示“12.5V”温度显示“25.6C”。这种组合显示的要点是单位字符固定写死数字部分动态刷新但刷新时要注意覆盖余位。举个例子第一行显示“Voltage: 1.2V”电压变成“12.5”时如果不把原来的空格和单位位置一起刷新屏幕上可能出现“Voltage: 12.5V”和残留的旧字符混在一起。我的做法是固定输出长度或者每次先写一段带格式的字符串char buf[16]; sprintf(buf, Voltage:%5.2fV, volt); Lcd_ShowString(0, 0, buf);格式化字符串把单位写在格式里这样每次刷新的内容长度一致就不会有残留。%5.2f表示宽度5位、保留两位小数正好覆盖“12.50”这样的最大长度。这个方法在串口输出里常见放到LCD1602上一样好用关键是“一次性构造完整行内容再整体刷到屏幕”。4. 局部滚屏的核心实现只让该动的部分动4.1 为什么LCD1602能滚屏DDRAM远不止16列LCD1602表面上只有2行×16列但DDRAM实际上是每行40个字符的缓冲区只是屏幕相当于一个滑动的窗口默认显示这40个字符的前16个。这才是“骚操作”的底气所在你完全可以把一行文字写到DDRAM的第0x10到0x17位置然后通过不断改变DDRAM地址来实现“视口滚动”的效果而不需要真正移动整屏内容。很多人第一次听到这个原理时都会愣一下因为看实物就是16列根本没想过后面还藏着24列缓冲区。这也是LCD1602能做滚屏显示的基础。如果是整屏滚动可以用HD44780的移位命令0x18或0x1C让整个显示内容左右移动。但实际项目中更多时候是“局部滚屏”比如第一行固定显示菜单第二行滚动显示长文本或者第一行显示标题第二行循环播放一条状态信息。这时候用整屏移位指令就废了因为它会连第一行一起动。正确的思路是“手动搬字符”。4.2 手动实现第二行局部滚屏的两种方法方法一DDRAM地址推进法。第二行的DDRAM地址范围是0x40到0x4F对应屏幕可见区0x50到0x67是屏幕外缓冲区。把一串字符从0x40位置开始连续写入然后每次把写入起始地址加1相当于把整行内容在缓冲区里向左滑动。但问题来了HD44780的地址设置指令一次只能设置一个绝对地址地址自动递增也只能在写数据时往地址更大的方向走你没法通过一条指令让第二行整体左移。所以这个方法更适用于“从右往左推入新字符”的场景比如做一条跑马灯式的信息条。方法二读改写搬移法。先把当前行所有16个字符读出来从第1列开始向左搬一格第16列写入新字符再把整行重新写回屏幕。这个方法需要读DDRAM而读操作的时序和写不一样要先把RW拉高、读地址设置好再读数据。如果不想处理读时序也可以在内存里维护一个长度为16的数组每次滚动前在数组层面左移再把整个数组刷到屏幕。后者更简单也更适合51单片机这种低端MCU。我实际做项目时倾向用数组法。下面这段代码是一个“第二行循环显示一段长文本”的实现第一行完全不受影响unsigned char line_buf[17]; // 16字符 结束符 void Lcd_Scroll_Line2(unsigned char *str, unsigned int length, unsigned int step) { unsigned char show_buf[17]; unsigned int i; // 每次从str中取16个字符起始位置由step控制 for(i 0; i 16; i) { show_buf[i] str[(step i) % length]; } show_buf[16] \0; Lcd_ShowString(1, 0, show_buf); }调用时main函数里每隔一段时间step再次调用Lcd_Scroll_Line2就能看到第二行文字平滑左移。% length取模让滚动可以无限循环实现类似LED点阵屏的循环滚动效果。屏幕第一行显示什么完全不受影响。4.3 上下两行分工固定菜单 滚动数据的完整示例给一个更贴近实际项目的场景。设备状态界面第一行固定显示“Set: T25C H60%”第二行显示一条超过16字的状态信息比如传感器预热提示、系统运行记录需要循环滚动。这个界面的核心是刷新频率的控制。LCD1602的写入速度不算快如果每几十毫秒就刷一遍屏幕会闪烁。经验做法是滚动间隔控制在200ms以上一屏16个字符整行重写一次耗时约1毫秒左右肉眼是看不出闪烁的。但如果每刷新一次就清屏那闪烁感会非常强。所以在滚动函数里我只重写第二行不调用清屏指令。这点很重要清屏指令会把第一行的内容连同第二行一起清掉然后又得重写第一行闪得更厉害。void main(void) { unsigned int step 0; unsigned char msg[] System running OK | Temp 25.6C | Humidity 60%RH | ; Lcd_Init(); Lcd_ShowString(0, 0, Set: T25C H60%); while(1) { Lcd_Scroll_Line2(msg, sizeof(msg) - 1, step); step; Lcd_Delay(60000); // 大约几百毫秒的延时 } }这套结构做出来之后你再看LCD1602的滚屏需求其实就是“往指定的DDRAM地址写字符”的循环过程区别只在于地址怎么步进、内容怎么取。把数据源换成温度、湿度、电压等实时值再加一两个按键控制方向一个完整的仪表界面就出来了。4.4 整屏移位指令的适用场景整屏移位命令0x18左移和0x1C右移其实更适合“开场动画”或者“整条信息平移”的场景比如开机时让LOGO从屏幕左边滑进来。它会把屏幕上的所有内容一起移动前提是你希望所有行一起动。局部滚屏不能用它因为一旦执行整屏移位固定菜单也跟着跑了。如果你只需要上下两行中某一行滚动记住一句话手动搬字符别碰移位指令。还有一个容易踩的坑执行整屏移位指令后DDRAM地址并没有变只是显示窗口的位置变了。如果你在滚动过程中去定位光标写新字符很可能会写到“看不见”的区域里去表现就是屏幕某处突然少了几个字。所以使用移位指令前要把文字内容全部写到位滚动过程中不要再穿插写操作。5. 常见问题与排查技巧实录LCD1602的坑前人踩过无数遍我把频率最高的几类问题按现象、原因、解决方法列成了表方便大家直接对照排查现象原因解决方法上电后屏幕白屏没有任何字符对比度电位器没调好VL引脚电压不合适慢慢旋转对比度电位器使VL电压约0.5V左右能看到字符轮廓为止屏幕上只有一排方块初始化时序不对功能设置指令没被正确识别确认0x38、0x0C、0x06、0x01的顺序上电延时至少15ms以上第一行正常第二行乱码第二行地址计算错误漏加了0x40检查Lcd_SetCursor里row为1时addr是否加上0x40数字显示成乱码直接把数字送给驱动没有转ASCII码用0num或sprintf转换滚屏后固定行出现残留字符滚屏过程把整屏清掉或者滚动行的旧字符没有覆盖不要用清屏指令滚动行整行重写并保证长度一致光标不显示显示开关指令用的0x0C光标被关闭需要光标时改用0x0E或0x0F写数据时屏幕闪得厉害每次刷新都执行了清屏指令或者刷新间隔太短去掉清屏只重写变化区域滚动间隔大于200ms数据口用P0但没加上拉电阻P0是开漏输出驱动不了LCD的数据线P0口需要外接4.7kΩ~10kΩ上拉电阻或者改用P1/P2口有一个我屡试不爽的排查技巧准备一个只显示固定字符串的最小测试函数比如在左上角显示“LCD OK”。如果这都显示不出来问题一定在底层驱动或者硬件连接如果固定显示正常再去查光标、数字、滚屏这些上层逻辑。这种分层定位的方法比自己在那儿瞎猜哪里写错了要高效得多。再补充一个关于延时的经验。LCD1602的使能引脚E需要至少几百纳秒的高电平脉冲很多代码里直接用LCD_E 1; LCD_E 0;而没有中间延时这在12MHz晶振下勉强能用但并不稳定。我的驱动里每步都加了Lcd_Delay(100)虽然慢一点但可靠性大幅提升。如果对刷新速度有要求可以把延时常量调小但不要完全去掉。还有一点关于上拉电阻。STC89C52RC的P1口是准双向口内部有上拉接LCD1602数据线不需要外部上拉。但如果你用的是P0口必须接上拉电阻否则数据线的电平根本拉不上去屏幕要么白屏要么显示乱码。很多初学者在一个最小系统板上折腾半天没点亮屏幕最后发现就是P0口缺了上拉电阻这个细节比代码本身更容易坑人。结尾说说我调式这块屏的体会我在实际项目中把LCD1602从最简单的“显示一行字”一路做到“光标定位输入、双行分工、局部滚动”最大的体会是这块屏看着简单但把DDRAM地址和指令集吃透之后能玩出的花样远超预期。很多人一遇到显示问题就怀疑屏坏了或者代码抄错了其实百分之八十的情况都是地址算错、时序不稳、或者对比度没调好。最后再分享一个小技巧写地址相关代码时先把行列参数换算成DDRAM地址用表格列出来写在代码注释里调试时对着表查速度快很多。LCD1602的驱动代码真没有必要满世界找自己按需定制才是最长久的解决方案。
返回列表