简介:这是一套面向C51单片机学习者与电子爱好者的LCD1602显示例程合集。例程围绕矩阵按键键值显示、DS18B20温度读取、DS1302时钟时间显示以及ADC0832电压转换这四类典型应用,演示了如何通过单片机控制字符型液晶屏将数据直观呈现,可直接借鉴到课程设计或日常练习中。压缩包共八十六个文件,大小约四点一三兆,内部包含C语言源码、Keil工程、Proteus仿真原理图、HEX固件以及多段仿真演示视频,既能直接运行验证,也可拆解学习。已有超过一千人学习下载,适合正在学习单片机外设驱动的入门读者。四个独立例程目录均配有工程与录屏,读者可以分别练习按键扫描、单总线通信、串行时钟读写和模数转换解析,同时理解液晶屏的初始化流程、显示地址设置和读写时序;源码结构清晰,关键函数注释到位,为后续嵌入式项目开发打下扎实基础。
1. 项目概述:LCD1602与C51的组合为什么是经典
LCD1602这块屏幕,玩单片机的朋友几乎没有不认识的。它属于字符型液晶模块,一次能显示两行,每行16个字符,通过内部集成的HD44780控制器驱动。C51单片机(主要指基于8051内核的STC89C52、AT89S52这类芯片)之所以常年和它绑在一起,是因为两者在硬件接口上天然匹配:1602的工作电压是5V,C51的I/O口电平也是5V,不需要额外加电平转换芯片;1602的并口时序也比较宽松,C51的机器周期虽然慢,但慢有慢的好处,用软件延时模拟时序完全来得及。
这个项目解决的核心问题是:如何让一块单片机控制屏上按指定位置显示我们想要的字符、字符串,甚至自定义符号。有人可能会说,现在都上OLED、TFT彩屏了,为什么还要折腾1602?我的看法是,1602的价值在于它把“并行数据传输”“时序匹配”“控制器指令集”这三个底层概念完整地暴露在开发者面前。你把这个例程吃透了,后面上手OLED(SPI/I2C)、TFT屏幕,很多原理是相通的。而且对新手来说,用逻辑分析仪看1602的时序波形,比分析I2C协议要直观得多。
本文适合三类人:刚学完C51基础(跑马灯、按键、中断)想进阶的初学者;需要快速在项目里挂一块文字屏展示数据的开发者;以及想搞懂液晶底层驱动原理的同学。我会把硬件接线、时序原理、完整例程和排错经验一次讲清楚,这篇文章里的代码我实际烧录验证过,可以直接抄。
2. 硬件接线与工作原理拆解
2.1 LCD1602引脚功能与接线方案
LCD1602有16个引脚,标准排列如下:
| 引脚号 | 符号 | 功能说明 | 接法建议 |
|---|---|---|---|
| 1 | VSS | 电源地 | 接GND |
| 2 | VDD | 电源正极 | 接+5V |
| 3 | VO | 液晶对比度调节 | 接电位器中点,两端接5V和GND |
| 4 | RS | 寄存器选择:高电平选数据寄存器,低电平选指令寄存器 | 接P1.0 |
| 5 | RW | 读写选择:高电平读,低电平写 | 接GND(只写不读) |
| 6 | E | 使能信号,下降沿锁存数据 | 接P1.1 |
| 7-14 | D0-D7 | 双向数据总线 | 接P0.0-P0.7 |
| 15 | A | 背光正极 | 串联电阻接5V |
| 16 | K | 背光负极 | 接GND |
这里有两个地方要特别注意。第一,VO引脚不建议直接接地,虽然很多教程图省事这么干,但对比度会过高或过低,屏上要么只有方块没有字,要么灰蒙蒙一片。标准做法是接一个10K电位器,调节到能清晰看到两行小方块为止。第二,RW引脚直接接地是有前提的——你只需要向屏幕写数据,不需要从屏幕读状态。绝大多数例程都是这种“只写模式”,因为1602的忙标志检测要从D7脚读数据,接法会多一根线,代码也麻烦。只要你的延时时间足够充裕,写操作就不会撞上忙状态,直接接地完全可行。
2.2 HD44780控制器的核心机制
LCD1602能显示字符,靠的是内部的HD44780控制器。这块芯片的工作方式其实就是一个带有自动地址指针的RAM:屏幕上的每个字符位置对应一个DDRAM地址,你往哪个地址写数据,字符就出现在那个位置。第一行地址从0x00到0x0F,第二行从0x40到0x4F。
HD44780有两套寄存器:指令寄存器(IR)和数据寄存器(DR)。RS引脚就是用来区分这两套寄存器的。你在操作时,本质上是在做两件事:要么发一条指令告诉它“我要做什么”,要么发一个字符数据让它显示。
常用指令集如下:
- 0x38:功能设置,8位数据总线、两行显示、5x7点阵字符
- 0x0C:显示开关控制,开显示、关光标、不闪烁
- 0x01:清屏,光标归位
- 0x06:输入模式设置,写入新数据后光标右移,整屏不移动
- 0x80 + 地址:设置DDRAM地址,即光标定位
每条指令的执行都需要一定的处理时间。清屏指令0x01需要1.64ms,其余指令约40us。用12MHz晶振的51单片机,软件延时100us以上就够用了。这就是为什么很多例程在每条指令后都做较长的延时——不是浪费时间,而是在等HD44780内部处理完毕。
2.3 时序图与“为什么是下降沿锁存”
1602的写操作时序是新手最容易懵的地方。很多人照着例程抄,E引脚一顿操作,屏幕亮了有字,但说不清为什么。
核心要点是:E引脚产生一个高脉冲,在脉冲的下降沿,1602锁存D0-D7上的数据。更完整的过程是:先把要写入的数据放到数据总线上,然后拉高RS确定是数据还是指令,再把E拉高,保持一段时间(至少需要ns级),最后拉低E。关键在于,数据必须在E下降沿之前就稳定在总线上。时序图里有一个“数据建立时间”参数,大约40到80ns,对C51来说,一条指令就要好几个us,这个时间绰绰有余。
用生活化的类比来说:E引脚就像相机的快门,数据就是被拍的对象。你对好焦(摆好数据)再按快门(拉高E),松手瞬间(拉低E)完成曝光。如果你按快门的同时还在移动对象,拍出来的照片就是糊的。
3. 完整例程解析与实操验证
3.1 例程代码与逐模块讲解
这个例程我用的是STC89C52,晶振12MHz,开发板上P0口接1602数据线,P1.0接RS,P1.1接E。P0口是开漏输出,必须要接上拉电阻,这个后面排错环节会细说。以下是完整代码:
#include <reg52.h> #define LCD_DATA P0 sbit RS = P1^0; sbit E = P1^1; void delay_us(unsigned int us) { while(us--); } void LCD_WriteCmd(unsigned char cmd) { RS = 0; LCD_DATA = cmd; E = 1; delay_us(200); E = 0; delay_us(1000); } void LCD_WriteData(unsigned char dat) { RS = 1; LCD_DATA = dat; E = 1; delay_us(200); E = 0; delay_us(1000); } void LCD_Init(void) { delay_us(15000); LCD_WriteCmd(0x38); delay_us(2000); LCD_WriteCmd(0x38); LCD_WriteCmd(0x0C); LCD_WriteCmd(0x06); LCD_WriteCmd(0x01); delay_us(2000); } 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++); } } void main(void) { LCD_Init(); LCD_ShowString(0, 0, "Hello, LCD1602!"); LCD_ShowString(1, 1, "C51 LCD Test OK"); while(1); }代码的逻辑不复杂,但每段都有讲究。初始化函数里,0x38先写一次再写一次,是因为有些1602模块上电后状态不确定,多写一次确保进入8位模式。这一点在数据手册里有明确建议,属于HD44780上电初始化流程的标准操作之一。
LCD_WriteCmd和LCD_WriteData的唯一区别就是RS引脚的电平。RS=0时,1602把总线上的数据解析为指令;RS=1时,解析为要显示的字符ASCII码。你传进去一个字符串,比如“Hello”,实际上就是在循环发送H、e、l、l、o这几个ASCII码。1602内部会自动把ASCII码点阵化,显示在光标当前位置。
3.2 光标定位与显示偏移的规律
LCD_SetCursor这个函数网上有很多写法,本质都是在计算DDRAM地址。第一行起始地址是0x00,第二行起始地址是0x40。要显示在某个位置,就把基地址加上列号,然后OR上0x80。0x80是置位DDRAM地址指令的标志位,意思是告诉控制器“接下来的7位数据是你要定位的地址”。
我最初写这个函数时犯过一个错:用十六进制0x00加列号没问题,但到第二行我用0x40加列号时,发现从第5列起屏幕就不显示了。后来查资料才明白,1602第二行DDRAM地址并不是连续的——第二行前4个字符对应地址0x40-0x43,后面地址是跳变的。虽然实际例程里0x40加列号连续写能正常显示(因为控制器内部把缺口地址做了映射),但了解这个设计会让你在遇到“某行某列不显示”时更快定位原因,而不是怀疑屏幕坏了。
3.3 烧录验证与显示效果
代码编译后生成的hex文件,用STC-ISP软件通过串口烧进单片机。注意STC89C52烧录时要先断电再点击下载,上电瞬间才会进入ISP模式。烧录成功后,屏幕第一行显示Hello, LCD1602!,第二行从第1列开始显示C51 LCD Test OK。
如果你手头没有开发板,用洞洞板搭一个最小系统也可以跑起来:单片机、晶振电路(12MHz晶振加两个22pF电容)、复位电路(10uF电容加10K电阻)、P0口上拉电阻排、1602和电位器。这套硬件成本不到二十块钱,是目前验证显示例程性价比最高的组合。
4. 常见问题与排查技巧实录
4.1 屏幕不亮或只有背光
现象:背光亮,屏幕黑方块,调电位器也没变化。
原因一般有三个方向。先查VO对比度电压,用万用表量电位器中点对地电压,正常应该在0.4V到1.5V之间,太高太低都出不来字。再查E引脚是否有脉冲:用示波器或逻辑分析仪看P1.1,初始化代码执行时应该有密集的方波。没有示波器的话,可以写个while循环持续刷新显示,再用万用表交流档测E脚,有微弱电压说明有信号。最后检查RS和RW接线,尤其确认RW有没有接地——RW悬空或接错高电平,写操作根本进不去。
4.2 有黑方块但不显示字符,或者字符乱码
出现明显黑方块(第二行常见)通常是初始化没成功,控制器没有进入两行8位模式。经典的原因是上电后没有给充分的延时就开始发指令。HD44780上电到可接受指令之间有大约40ms的内部复位时间,如果你上电后立刻初始化,指令就丢了。例程里我写了15000us延时就够用,但如果你的电源模块上电慢,建议加大到50ms甚至100ms,这一步一定要够耐心。
乱码问题多半是RS线接错。如果RS接反,指令被当成数据显示,数据被当成指令执行,显示结果必然是乱码。另一个常见原因是数据线接错位,比如D0-D7接到P2口但代码写的是P0口,或者杜邦线松动。检查思路很简单:从代码定义的端口反查物理接线,别想当然。
关于P0口上拉,我再强调一遍:P0口内部没有上拉电阻,是真正的开漏结构,驱动能力为0。如果你把1602数据线接在P0口却忘了接上拉电阻,初始化时高电平上不去,屏幕就会表现出各种诡异现象——有时显示乱码,有时只有第一行有字,有时调电位器出现整屏随机方块。我就见过好几个初学者卡在这里一下午,最后查出来是上拉电阻没装。解决方法:加一个10K排阻,一端接VCC,另一端分别接到P0.0-P0.7就行。
4.3 显示位置不正确,字符跑到奇怪的地方
这个问题的根源几乎都在DDRAM地址计算上。我一个朋友调试时发现,第二行输入“ABCDEFGHIJKLMNOP”,但显示出来是“ABCDABCDEFGHIJ”。这说明他行地址和高4位标志搞混了。正确的定位方式是:base = (row == 0) ? 0x00 : 0x40; addr = 0x80 | (base + col); 也就是先用条件判断选基地址,再加列号,最后和0x80做或运算。
还有一个容易忽略的坑:1602显示字符时是全角还是半角。如果你用Keil写字符串,字符串里有中文或中文标点,Keil默认会把它们编译成GB2312编码,1602根本识别不了,显示出来就是乱码或者空白。字符串里只能用ASCII字符,包括英文字母、数字和英文标点。
4.4 屏幕闪屏或字符忽明忽暗
闪屏多数不是1602的问题,而是电源纹波。单片机驱动1602时,背光电流突变会导致VDD电压跌落,尤其在用USB转TTL模块供电的洞洞板电路上。处理办法:背光串一个100欧电阻限流,不用可免;在1602的VDD和GND之间加一个10uF到100uF的电解电容;确保单片机电源和1602电源来自同一路,不要分成两路。
如果是使用定时器中断的工程出现闪屏,要考虑是不是中断里做了耗时操作,导致主循环里屏幕刷新被频繁打断。
4.5 换了一颗单片机就显示异常
STC89C52和AT89S52在指令集上完全兼容,但有些国产51单片机复位时间、I/O口默认状态不同,可能导致同一个程序在不同芯片上表现不同。解决办法是初始化前加更长延时,并且把数据线的初始状态全部拉低。还有一个通用技巧:把LCD_Init里的延时从固定值改成循环调用,比如DelayMs(100),多试几个值,找到最稳定的组合。
5. 进阶扩展思路
以上把LCD1602的基础显示例程梳理完了,代码可以直接跑。不过我还想再分享两个在基础之外的思路。
第一个是使用P1、P2、P3口的4位模式。4位模式只接D4-D7四根数据线,能省四个I/O口,但代码里要先写高4位再写低4位,信号流程更复杂。如果你的项目I/O口紧张,值得把4位模式练一练。
第二个是把显示内容做成可变量。比如和DS18B20温度传感器组合显示当前温度,或者通过串口接收数据再显示到1602上。核心思路就是把要显示的内容先格式化到一个字符串数组里,再用LCD_ShowString调用。我用过sprintf函数做格式化,但注意Keil C51的sprintf比较占代码空间,单片机Flash型号够大才行。体积紧张的话,可以手写一个简单的数值转字符串函数,只处理整数和小数,几百字节就够用。
我在实际项目中踩过最深的一个坑,是“代码看着没问题但就是显示不出来”。后来逐行排查才发现,是开发板上1602的E引脚被复用成LED灯控制引脚了,初始化LED的代码把E脚电平拉低,干扰了液晶时序。这一点提醒大家:应用开发前先看原理图,确认引脚有没有被板载外设占用,这是省时间的好习惯。
最后分享一个小技巧:把LCD_WriteCmd和LCD_WriteData里的延时独立出来,在调试时用示波器对比“功能设置”“显示开关”“清屏”三条指令的时序波形,你能直观看到初始化是否按预期执行。这个习惯帮我解决过很多“看似玄学”的显示问题,也推荐你试试。
本文还有配套的精品资源,点击获取