ARTICLE DETAIL

资讯详情

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

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

LCD1602进阶:HD44780光标定位、数字显示与局部滚屏实战解析 LCD1602这块屏在嵌入式圈子里聊的人太多了。但说句实话网上铺天盖地的例程里九成都是“初始化显示字符串”的水平真到了项目里要做光标定位、要显示实时数字、要局部滚屏的时候光靠那点例程根本撑不住。我最近用STC89C52RC和LCD1602做一个简易仪表界面把这几块硬骨头啃了一遍踩了不少坑也总结了不少方法。这篇文章就是把HD44780控制器的底层机制彻底掰开告诉你光标定位、数字显示、局部滚屏这三个“骚操作”到底是怎么实现的。先说清楚LCD1602用的主控芯片是HD44780或者兼容片所有命令和数据都是通过8位并口也有4位模式送进去的。这篇文章的原理跟具体单片机型号无关STM32、Arduino、STC89C52RC全都通用。我会默认你已经能把屏点亮、能显示一行“Hello World”下面的内容是在那个基础上再往深挖一层。1. 先弄懂HD44780的DDRAM光标定位才能不死记硬背1.1 DDRAM的物理布局和隐藏区LCD1602内部维护着一块80字节的显示数据RAM英文缩写DDRAMDisplay Data RAM。从MCU这边看你往DDRAM的某个地址写入一个字节这个字节对应的字符就会出现在屏幕上对应的位置。这块80字节的区域被设计成每行40字节、共两行第一行的地址范围是0x00到0x27第二行的地址范围是0x40到0x67。很多教程在这里只说一句“第一行0x00开头、第二行0x40开头”就带过去了但其实里面藏着一个很关键的信息LCD1602虽然只露出16列但DDRAM每行给你准备了40个地址。前16个地址对应屏幕可见的列后面的24个地址属于隐藏区平时看不见也摸不着。就是这个隐藏区给后面的局部滚屏留了操作空间。你如果往隐藏区写字符屏幕上不会立刻显示但它确实占用了DDRAM的空间后面配合显示移位命令就能“滚”出来。1.2 地址指针与0x80命令MCU想让“下一个字符写到屏幕的哪个位置”唯一的手段就是设置DDRAM地址指针。HD44780有一条专门的指令Set DDRAM Address命令码是将0x80与目标地址相或。比如你要在第一行第3列写字符第一行第3列的DDRAM地址是0x0030x03于是发送0x80|0x03也就是0x83。要定位到第二行第5列地址是0x4050x45发送0xC5。这就是所有光标定位函数的底层逻辑。Arduino LiquidCrystal库里的setCursor(col,row)本质上就是把row换算成0x00或0x40的基地址再加上col最后调用这条命令。网络上能找到的LCD1602驱动代码版本很多但你要真想会用、会改最好还是自己从这条指令开始写一遍比抄任何现成驱动都管用。你甚至可以这么类比在电脑的文本框里用鼠标点击一下光标就跑到那个位置在LCD1602上往地址指针指到哪下一个写进去的字符就落到哪只不过这里的“鼠标”是纯软件控制的。这里还经常有人把DDRAM地址设置和CGRAM地址设置搞混。CGRAM是用于自定义字符的存储器设置它的命令是0x40加上6位地址跟第二行的0x40基地址完全是两码事。碰见“写入0x40开头的命令”别急着激动先看清楚是0x80打头还是0x40打头一个管显示位置一个管自定义字符搞反了屏幕就会出鬼。2. 光标定位的三层玩法从指令到函数再到项目应用2.1 一个标准的set_cursor实现下面这段代码是8位并口模式下我给STC89C52RC写的定位函数完整覆盖了初始化之后的日常使用场景。#define LCD_DATA P0 sbit LCD_RS P2^6; sbit LCD_RW P2^5; sbit LCD_E P2^7; void lcd_write_cmd(uint8_t cmd) { LCD_RS 0; // 命令模式 LCD_RW 0; // 写操作 LCD_DATA cmd; LCD_E 1; delay_us(1); LCD_E 0; delay_us(50); // HD44780命令执行时间 } void lcd_write_data(uint8_t dat) { LCD_RS 1; // 数据模式 LCD_RW 0; LCD_DATA dat; LCD_E 1; delay_us(1); LCD_E 0; delay_ms(1); // 数据写入DDRAM稍慢 } void lcd_set_cursor(uint8_t col, uint8_t row) { uint8_t addr; if (row 0) addr 0x00 col; else addr 0x40 col; lcd_write_cmd(0x80 | addr); }用的时候直接lcd_set_cursor(0, 1)再把要显示的数据写进去内容就出现在第二行最左边。这里有一个细节col的范围要控制在0到15因为16列就是可见区的极限。如果你传了16或者更大的值字符并不会跑到屏幕外而是会落到DDRAM的隐藏区屏幕上什么都看不到容易让人误以为函数坏了。2.2 光标显示和闪烁该不该开初始化的时候我习惯用0x0C这条命令开显示、关光标、关闪烁。但做菜单或者输入编辑的时候光标反而是很有用的提示。0x0E是开光标不开闪烁0x0F是光标加闪烁。闪烁用的是HD44780内部逻辑不用你写额外代码硬件每帧自动闪实现起来零成本。这里要单独提醒一下很多人以为光标定位就是把“光标”这个下划线移过去。实际上光标位置就是DDRAM地址指针的位置光标停在哪下一个要写入的字符就落在哪。做输入框的时候合理利用光标闪烁用户体验会好很多完全不比那些带背光的彩色屏差。唯一注意点是如果你开了光标闪烁写数据的时候光标会跟着地址移动某些场景下需要清掉光标再刷新这个后面讲问题排查时会再提。2.3 光标定位的三个项目实战细节第一覆盖写。想在某个位置刷新数字直接用lcd_set_cursor定位过去写新内容就行不用先清屏。但要注意新内容比旧内容短的时候要把剩余部分用空格覆盖掉否则会留下“尾巴”。这个问题在显示倒计时、开关状态这类频繁变化的信息时特别常见。比如上一次显示的是“1234”下一次要显示“56”如果只写了两个字符前面会残留“34”整个界面立刻乱掉。第二状态栏刷新。把固定内容比如“Temp:”写在DDRAM的固定地址变化的值写到固定内容后面。每次只刷新值区域固定内容不动整个界面看起来非常稳。我做的仪表界面就是第一行显示“Voltage: 12.50V”第二行显示“Current: 0.83A”每次刷新只改数字部分把定位函数用熟之后这个界面十分钟就能搭出来。第三菜单光标的移动。做菜单时我习惯把选中的行标记成“”取消选中时写回空格。核心操作就是两次lcd_set_cursor加一个字节的写入配合按键扫描很容易实现。注意按键扫描通常有消抖延时如果光标闪烁还用延时消抖视觉上会卡一下建议把菜单逻辑里的延时尽量控制在10毫秒以内。3. 显示数字把数值变成字符才是关键3.1 LCD1602只认ASCII字符LCD1602不是数码管它内部字符生成器决定了它只能直接显示字符编码最常见的就是ASCII。数字0到9在ASCII表里的编码是0x30到0x39。如果你想显示数字5往DDRAM里写入的不是0x05而是0x35也就是字符“5”的编码。这个错误几乎每个新手都犯过用一个int变量存了个数值直接往LCD里写结果屏幕上要么是乱码要么什么都没有。原因很简单MCU把那个整数值当成字符编码去解释了。你写lcd_write_data(5)控制器会把它当成ASCII控制字符ENQ来处理不会显示任何可见符号。正确做法是先把数值5转成字符“5”转换的数学关系就是加0x30因为0x3050x35。这种“数值加偏移转字符”的思路在显示十六进制、显示BCD码时都一样通用。3.2 手写一个数字转字符串函数我项目里常用的数字显示函数长这样支持负数最大显示5位数足够覆盖绝大多数仪表场景void lcd_put_int(int16_t num) { char buf[6]; int8_t i 0; uint8_t neg 0; if (num 0) { neg 1; num -num; } if (num 0) { lcd_write_data(0); return; } while (num 0) { buf[i] 0 (num % 10); num / 10; } if (neg) buf[i] -; while (i 0) lcd_write_data(buf[--i]); }思路是不断取余10拿到个位数字加上0x30得到对应字符存进缓冲区最后倒序依次写出去。负数就加个符号在倒序输出之前处理。如果你要显示固定位数比如倒计时“009”在主调函数里判断一下当前数字位数不足就补空格或补0处理起来很灵活。这个函数比sprintf轻量太多在51这种Flash和RAM都紧张的单片机上非常实用移植到STM32时也完全能跑。3.3 sprintf在51上为什么能少用就少用当然你也可以用sprintf拼接字符串再来显示像这样char buf[16]; sprintf(buf, Val:%d, vol); lcd_puts(buf);在STM32上这么写没毛病但在STC89C52RC上我强烈建议提前确认编译器有没有把你引入的printf系列函数完整链接进来。Keil C51默认情况下这些函数的完整版会占好几KB的Code空间而STC89C52的Flash只有8KB你还得装驱动、装主逻辑空间非常紧张。更麻烦的是printf的格式化参数还依赖标准库的字符输出重定向用不好经常出现“编译过、烧录后没反应”的诡异问题。所以我的原则是能用简单数字转换就用转换非要格式化字符串也只在RAM和Flash都富余的单片机上用sprintf51上尽量绕开。显示屏只有16列2行本来能显示的信息量就有限格式化字符串的场景真没那么多。3.4 和7段数码管的对比很多做单片机练习的人会在7段数码管和LCD1602之间纠结。这俩显示数字的思路完全不一样7段数码管是段驱动你要把一个数字拆成7段a到g的亮灭组合查表或计算然后并行/串行送出去LCD1602是字符写入你把ASCII字符送到DDRAM字符的字形由HD44780内部的字符生成器负责。拿显示数字0到9这件事来说LCD1602代码更简单而且能同时显示多位数字、小数点、字母数码管要复杂得多。不过数码管也有它的优势亮度高、响应极快、可以在强光下用而且一颗MCU可以同时驱动多位、刷新频率高的时候很流畅。LCD1602的刷新率受限于HD44780本身写字符还要一点等待时间做高速计数显示时容易看到残影。选哪个取决于场景不能只看“哪个好看”。在仪表类应用里LCD1602明显更合适因为你能在数字旁边显示单位、标签、甚至画简单的进度条。4. 局部滚屏硬件全局移位与软件窗口重绘4.1 硬件移位命令0x18/0x1C的真相HD44780有一组命令专门用来移动显示内容和光标移动是分开的两套逻辑。光标移动命令是0x10、0x14分别把光标左移、右移显示内容不动。显示移位命令是0x18、0x1C分别把整个显示区域往左、往右移一格光标同时跟着走。这组命令在很多跑马灯教程里出现过做法通常是在DDRAM隐藏区里预先塞一段长文本然后连续发0x18就能看到文字从右往左滚进来。很多人用这组命令做滚屏觉得方便但有个隐藏问题它移动的是整个屏幕也就是说第一行和第二行会一起往左滚。你想让第二行滚动而第一行的标题保持在原位硬件层面做不到。另外0x18/0x1C是把显示器窗口往DDRAM的另一个地址移动滚动几次之后DDRAM地址指针和屏幕显示内容的对应关系就变了。这时候你再用lcd_set_cursor去想当然地定位很容易写偏。所以这个命令适合整屏滚动不适合“局部”两个字。4.2 真正的局部滚屏软件窗口重绘法要在LCD1602上实现真正的局部滚屏我用的做法是“软件窗口重绘”。原理很简单设定一个显示窗口比如第二行的第6列到第15列共10个字符。每帧刷新时我从长字符串里取出对应窗口位置的那10个字符重新写到这一区块。因为写10个字符的时间很短肉眼基本看不出刷新痕迹只要刷新速度别太夸张效果就和真的局部滚动一样。这个方法的优点是一点都不依赖硬件移位窗口以外的区域完全不受影响第一行的标题、第二行前面的状态栏都稳稳不动。缺点是每一帧都要重写窗口内容理论上会有轻微的光标闪烁痕迹实际体验下来只要不把刷新周期压到几十毫秒以内完全看不出来。我一般用300毫秒到500毫秒的刷新间隔滚动既流畅又稳定。4.3 完整例程单行局部滚屏下面是完整的局部滚屏示例假设第一行显示“Fixed Title”第二行从第6列开始滚动长文本#define WIN_START 6 #define WIN_LEN 10 code char ticker[] LCD1602 local scroll demo by HD44780...; void lcd_update_window(uint16_t offset) { uint8_t i; uint16_t len sizeof(ticker) - 1; // 去掉结尾的\0 lcd_set_cursor(WIN_START, 1); for (i 0; i WIN_LEN; i) { lcd_write_data(ticker[(offset i) % len]); } } void main() { uint16_t offset 0; lcd_init(); lcd_puts(Fixed Title); // 第一行固定内容 while (1) { lcd_update_window(offset); offset; delay_ms(300); // 控制滚动速度 } }这里用取模实现环形循环offset每帧加一窗口里的每个字符都基于(offseti)取到这个长字符串的对应位置。因为窗口长度是10只要字符串总长大于10滚动效果就是连续的。300毫秒的节奏滚动速度比较和缓当告警信息或者跑马灯用都很合适。你还可以灵活调整WIN_START和WIN_LEN把窗口放在第一行、放在中间、或者开两个窗口同时滚两段内容都行只要窗口区不重叠就行。如果把lcd_update_window改成每帧先清一下窗口再写会有很明显的闪烁所以我推荐直接整块重写不清窗口。字符长度相同的情况下覆盖写完全不会残影这是我在十几个项目里验证过的经验。5. 常见问题与排查技巧实录5.1 典型故障速查表现象最常见原因排查方法写到第二行没反应行地址没加0x40检查lcd_set_cursor里row1时是否有addr0x40col显示数字变成乱码直接把int数值当字符写确认写完数字前先加0x30或用数字转字符串函数局部滚屏时整屏都在动用了0x18/0x1C硬件移位换成软件窗口重绘法别用显示移位命令光标定位后内容跑到别处初始化用了entry mode里的S位初始化命令统一用0x06别开自动移位屏幕显示满格方块对比度电位器没调好调节Vo引脚的电位器直到字符清晰一上电屏幕空白初始化时序太快或供电不稳上电后先延时15毫秒以上再发初始化命令写了字符但位置偏一位没等E脉冲宽度或写命令间隔太短确认E引脚脉冲至少1微秒命令间隔尽量放宽局部刷新看到残影覆盖内容比旧内容短刷新前用空格填充旧内容剩余部分5.2 几招亲测有效的避坑经验第一初始化顺序不要乱。HD44780对初始化时序有要求上电之后要等至少15毫秒然后发功能设置0x38再延时几毫秒接着发0x0C、0x01、0x06。很多乱码问题不是代码逻辑错就是初始化时序太紧。我用的标准初始化是这样延时20毫秒0x38延时5毫秒0x38延时5毫秒0x38延时5毫秒0x0C清屏0x01最后0x06。虽然重复发了好几次0x38看着冗余但这是HD44780数据手册明确要求的可靠启动方式。第二4线模式省引脚但别在调试时用。4线模式能省4个IO但发送一个字节要拆成两次调试时容易看花眼。如果你不是引脚紧张到极点新手阶段优先用8线模式等所有逻辑调通了再改4线不迟。改4线时尤其注意高4位和低4位的数据排列很多人的4线驱动乱码都是高低位顺序搞反了。第三驱动代码别到处找自己按数据手册写。网上确实能下载到各种LCD1602驱动代码但很多是老古董时序、引脚定义都跟你的板子对不上。HD44780的指令就十来条自己写一遍不超过100行比调别人的代码省心得太多。Arduino的LiquidCrystal库源码也完全是开放的遇到不懂的指令翻源码比翻二手教程更靠谱。我个人在实际项目里还有一个习惯所有显示内容的更新都集中到统一的刷新函数里状态标志变了才调用而不是在主循环里反复刷。这样既能减少不必要的写入也能避免光标闪烁界面干净很多。LCD1602虽然是老外设但只要你把DDRAM、指令集、地址指针这些底层的逻辑吃透光标定位、数字显示、局部滚屏这些操作组合起来完全能做出让外人一眼看不出来是单片机实现的仪表界面。
返回列表