简介LCD1602与C51单片机结合的显示例程包面向单片机初学者与电子制作爱好者集中展示字符液晶屏在典型嵌入式场景中的驱动方法。压缩包共86个文件约4.13MB包含4个独立工程矩阵按键键值显示、DS18B20温度显示、DS1302时钟显示、ADC0832电压显示每个工程均含Keil工程文件uvproj/uvopt、C源码、Proteus仿真图DSN及Hex烧录文件另有4个对应仿真演示视频MP4和编译中间文件便于对照验证。包体虽小却完整覆盖单总线温度读取、实时时钟通信、模数转换采集与矩阵按键扫描等关键接口技术。目前已有1074人学习下载。通过逐一运行各分模块能直观理解LCD1602时序控制流程也能为课程设计或入门实战提供可直接参考的模板。1. LCD1602 C51这份显示例程到底能帮你省多少事第一次点亮 LCD1602 的时候我对着数据手册折腾了两天——不是不显示就是满屏方块。后来拿到一份写得比较完整的 C51 例程照着初始化顺序和时序一改十分钟就跑通了。这份「LCD1602C51单片机的显示例程」就是这类东西它不是什么高深理论而是把初始化、写指令、写数据、光标控制、字符串显示这些最常用的底层操作全部封装好你只要会用 Keil 打开工程改改引脚定义就能在 1602 上看到自己想要的内容。它解决的核心问题有两个一是帮你绕开 LCD1602 那套 HD44780 时序协议的入门门槛二是给你一套可以直接改的模板——想加按键、想接 DS18B20 显示温度、想接 DS1302 显示时钟都在这套代码的基础上往上叠。适合刚学完 C51 语法、正准备做课设或小项目的开发者也适合那些被显示模块卡住、急需一个可靠驱动模板的从业者。下面我会把这份资源拆开讲透从驱动原理到每一行关键代码再到我实际调试时踩过的坑确保你拿到手就能复现。2. LCD1602 驱动原理先搞懂 HD44780 时序再谈显示2.1 为什么 LCD1602 不是直接写数据就能显示LCD1602 内部核心控制器是 HD44780它不像数码管那样送个段码就亮而是分「指令」和「数据」两类操作通过 RS 引脚区分RS0 时写入的是指令比如清屏、光标移位、功能设置RS1 时写入的是要显示字符的 ASCII 码或自定义字模。这意味着你写任何显示逻辑之前必须先完成一系列初始化指令把显示模式、光标状态、数据位宽定下来。初始化顺序里最容易出错的是「功能设置」这条指令。8 位数据总线模式下你要发送 0x3800111000B它同时指定了 DL18 位接口、N1两行显示、F05x8 点阵字符。如果你用的是 4 位模式那条初始化序列就更讲究——要先发 0x33 再发 0x32分两次把接口切到 4 位中间还要穿插延时。很多例程模板默认走 8 位模式因为连线简单、时序容易满足我这份资源里的主驱动也是按 8 位写的但 4 位的兼容函数也留了接口。然后是读写时序。HD44780 的每个操作都要求 E使能端产生一个下降沿你要先把 RS、R/W、数据线电平准备好然后拉高 E保持一段时间后再拉低数据在 E 的下降沿被锁存。这个「先准备、再拉高、最后拉低」的先后顺序不能反否则就是黑匣子——表现为你代码逻辑全对但屏幕死活不出字。后面避坑章节我会专门讲一个和 E 引脚相关的典型翻车现象。2.2 引脚连接与硬件选型哪些必须接、哪些可以省标准的 16 引脚 LCD1602 里真正必须接到单片机的是 11 根RS、RW、E、D0-D78 位模式或者 RS、RW、E、D4-D74 位模式。剩下的是电源、背光和对比度调节。这里有个大家常犯的误区RW读写选择引脚不是必须接单片机的如果你只用写操作直接把它接地就行。很多最小系统板为了省 IO就把 RW 固定接地这样你的驱动函数里只需要控制 RS 和 E。背光 A/K 引脚要串一个限流电阻一般 10-100 欧姆具体看你背光模块的工作电流。V0 是液晶对比度调节脚接一个 10K 电位器到地调到屏幕上字迹最清晰又不出现重影的位置。这个电位器值得买因为每个屏的最佳对比度电压都不一样固定电阻很难一次调好——我见过有人用固定电阻接 V0结果屏幕显示极淡折腾半天还以为是代码问题。数据位宽的选择上我一般建议新手先走 8 位模式逻辑直观、时序余量大不容易出问题。等你想省 IO 给 DS18B20 或 DS1302 用的时候再切换成 4 位模式。8 位模式占 P0 或 P2 的 8 个口加上 RS、E 两个控制口一共 10 个 IO4 位模式只需要 6 个 IO省下来的口刚好够接一个 DS18B20单总线加一个 DS1302三线。这也是这份例程里「按键 温度 时钟」三个扩展能同时跑起来的原因。2.3 初始化流程拆解每条指令在干什么HD44780 上电后不会自动进入可用状态必须按固定顺序送指令。我参考的例程里初始化函数是这样的处理顺序第一步延时等待上电后至少等 40ms 让 LCD 内部逻辑稳定第二步发送 0x30 三次8 位模式下的唤醒序列每次都间隔 5ms 以上第三步发送 0x38 设置 8 位、两行、5x8 点阵第四步发送 0x08 关闭显示第五步发送 0x01 清屏这个操作耗时长要等 2ms 左右第六步发送 0x06设置写入后光标自动右移、整体不移动第七步发送 0x0C开显示、关光标、不闪烁。0x08 和 0x0C 这两条很容易被省略或颠倒。有些人上电直接 0x38 0x01 0x06 就开写结果屏幕上是乱码。原因在于如果不先关显示内部 DDRAM 里的随机数据会直接显示出来而 0x0C 里的低三位控制光标和闪烁如果你用了 0x0F开显示 光标闪烁就会出现一个方块光标跟着字符跑扫描出来很难看。例程里的 Lcd_Init() 函数把这七步按顺序写死了你不需要理解每条指令背后的状态机也能用但如果你想自定义显示效果——比如要两个光标、要整屏左移——就需要去查 HD44780 指令表改对应位了。3. 从例程到自己的工程驱动代码移植与第一个显示程序3.1 例程文件结构拿到压缩包后先看哪几个文件解压这份「LCD1602C51单片机的显示例程」后核心文件是 lcd1602.h 和 lcd1602.c另外会有 main.c 存放演示代码。有些版本还会附带 delay.c 做精确延时。我建议你先打开头文件把每个宏定义看一遍#ifndef __LCD1602_H__ #define __LCD1602_H__ #include reg52.h // 引脚定义区——这里根据你的原理图修改 #define LCD_DB P0 // 数据口8位并行数据接P0 sbit LCD_RS P2^6; // RS: 指令/数据选择 sbit LCD_RW P2^5; // RW: 读写选择常接地可省 sbit LCD_E P2^7; // E: 使能端 #define LCD_8BIT_MODE 1 // 18位模式, 04位模式 void Lcd_Init(void); // 初始化序列 void Lcd_WriteCmd(unsigned char cmd); // 写指令 void Lcd_WriteData(unsigned char dat); // 写数据 void Lcd_SetCursor(unsigned char row, unsigned char col); // 光标定位 void Lcd_ShowString(unsigned char row, unsigned char col, unsigned char *str); // 显示字符串 #endif这段代码里面最需要改的是LCD_DB、LCD_RS、LCD_RW、LCD_E这四个定义。不同的开发板引脚映射不一样有人用 P1 做数据口有人把 RS 接在 P3^5你要是直接编译大概率是能过的但显示会完全错乱。把这几行改成自己板子的实际接线是整个移植过程里唯一不能偷懒的地方。注意sbit只能用于可位寻址的引脚像 P2、P3 这些都可以如果你把 RS 接到 P0 口的某个引脚就得改用别的办法。3.2 写命令和写数据的核心逻辑E 下降沿的精确控制lcd1602.c 里最关键的两个函数是两个写操作。它们看起来简单但时序顺序错一步就不工作void Lcd_WriteCmd(unsigned char cmd) { LCD_RS 0; // RS0: 指令模式 LCD_RW 0; // RW0: 写模式如果RW接地这行无所谓 LCD_DB cmd; // 数据线上放指令 LCD_E 1; // E拉高 Delay10us(); // 高电平保持让LCD采样 LCD_E 0; // E拉低下降沿锁存指令 } void Lcd_WriteData(unsigned char dat) { LCD_RS 1; // RS1: 数据模式 LCD_RW 0; LCD_DB dat; LCD_E 1; Delay10us(); LCD_E 0; }这两个函数的核心就是那句「先放数据到总线上再拉高 E延时后拉低」。E 的下降沿是 HD44780 的锁存时刻所以数据必须在下降沿之前稳定存在总线上。你可能会想为什么 E 拉高后要延时 10us而不是立刻拉低因为 LCD 内部需要一段时间把总线电平读进去拉高后立刻拉低数据线上电平可能还没来得及稳定就会偶发显示错字。这个延时也不是越长越好E 电平持续时间过久会影响指令执行频率10us 在 12MHz 晶振下一般是安全的。还有一个细节LCD_DB cmd之后、LCD_E 1之前要不要加延时在 8 位模式下写 P0 口要留意——P0 内部没有上拉电阻如果你外部也没接上拉数据线上高电平会偏弱。很多开发板 P0 都带了 10K 上拉排阻那没问题如果没带你需要确保 LCD 模块的数据脚本身有上拉否则某些位读出来是 0显示的字就是乱的。3.3 第一个主程序清屏、定位、显示一行字符驱动封装好之后主程序就非常简单了。例程里的 main.c 大概长这样#include reg52.h #include lcd1602.h void main(void) { Lcd_Init(); // 初始化LCD Lcd_ShowString(1, 0, Hello, LCD1602!); // 第1行第0列显示 while(1) { // 主循环可以放其他任务 } }Lcd_ShowString 内部实现是先把光标定位到指定行列然后循环调用 Lcd_WriteData 把字符串逐个写进去。定位函数 Lcd_SetCursor 需要你了解一个映射关系1602 的 DDRAM 地址不是从 0 连续排到 79 的第一行地址是 0x00-0x0F第二行是 0x40-0x4F。所以定位第二行第一个字符时要传入 0x40 0 的地址也就是 0x80 | 0x40。例程里封装的 Lcd_SetCursor 函数内部做了这个转换void Lcd_SetCursor(unsigned char row, unsigned char col) { unsigned char addr; if(row 0) addr 0x00 col; // 第一行基地址 0x00 else addr 0x40 col; // 第二行基地址 0x40 Lcd_WriteCmd(0x80 | addr); // 0x80是DDRAM地址设置指令的标志位 }注意 col 的范围是 0-15超出这个范围地址就溢出到显示区外面了。这个函数表面上简单但它是后续所有显示功能的基础——你显示温度和时钟时每一行的位置都靠它控制所以值得仔细确认自己是否理解了这个地址换算逻辑。4. 从静态显示到交互按键扫描、DS18B20 温度与 DS1302 时钟的接入4.1 按键 菜单切换在显示例程上叠加交互逻辑纯显示例程只能算半个产品加上按键才有交互感。这份资源里通常包含至少两个按键的例程——一个切换显示页面一个调整参数。按键扫描的代码思路是检测 P3 口对应引脚电平消抖后在主循环里改变一个 page 变量Lcd_ShowString 根据 page 值决定显示哪组内容。sbit KEY1 P3^1; // 按键1切换显示页 sbit KEY2 P3^0; // 按键2预留调整用途 void Key_Scan(void) { if(KEY1 0) // 检测到低电平 { Delay10ms(); // 消抖10ms通常足够跳过机械抖动 if(KEY1 0) // 二次确认还是低电平 { page; if(page 2) page 0; while(KEY1 0); // 等待松手避免一次按压触发多次 } } }这里有两个坑我要提前说。第一个是消抖延时和 while 等待松手不能省——不消抖会出现一次按压 page 连跳好几页不等待松手会按住时反复触发。第二个是「如果按键接的是上拉输入按下时读到 0如果是下拉输入按下读到 1」这个极性搞反了按键就反向工作。例程里默认按键接 P3 口且外部有上拉如果你的板子按下时读到 1把判断条件反过来即可。按键处理好之后整个程序结构就变成主循环里先 Key_Scan()再根据 page 值决定调用哪一组显示函数。这时候你才体会到 LCD1602 的「第二行地址 0x40」换算方便在哪里——每一页内容都是通过 Lcd_SetCursor Lcd_ShowString 组合输出的代码量很固定可预测。4.2 接入 DS18B20 温度显示单总线时序与 LCD 输出的配合DS18B20 是单总线器件只有一根数据线时序要求严格——初始化、读位、写位都有微秒级的窗口。在这份例程里DS18B20 驱动和 LCD 驱动是分开的两个文件main.c 里负责调用。核心主流程是初始化 DS18B20 → 跳过 ROM 匹配 → 启动温度转换 → 等待转换完成 → 读取温度值两个字节→ 拼接成实际温度 → 格式化到 LCD 显示。读取温度的核心代码是这个样子bit DQ P3^3; // DS18B20 数据线定义 void DS18B20_Convert(void) { DQ 0; DelayUs(480); // 复位脉冲480us以上低电平 DQ 1; DelayUs(60); // 等待DS18B20应答 } unsigned int DS18B20_ReadTemp(void) { unsigned char low, high; unsigned int temp; DS18B20_Convert(); // 启动转换后用“读暂存器”指令读取 // 这里省略了具体读写时序函数例程里有完整实现 low DS18B20_ReadByte(); // 温度低字节 high DS18B20_ReadByte(); // 温度高字节 temp (high 8) | low; // 合成16位温度值 return temp; }DS18B20 返回的温度值分辨率默认是 12 位低 4 位是小数部分。要把这个 16 位数显示到 LCD 上需要做两步一是判断符号位bit15 为 1 是负温二是把整数部分和小数部分分离。例程里的格式化代码通常用temp 4取整数部分用temp 0x0F乘以 0.0625 得出小数部分用整数运算模拟浮点然后用 sprintf 或手动拼位把结果写到字符数组里。这里最容易卡住的是延时精度的匹配。DS18B20 的时序是微秒级的如果你的延时函数是用循环粗糙写的比如DelayUs(480)实际执行了 700us依然能工作但如果差太多比如只有 200us就会复位失败。例程里附带了一个基于定时器或者 NOP 指令的精确延时函数你移植时不要把延时函数换掉——这是血泪经验我见过有人为了省事把延时函数换成自己写的for(i0;i100;i);结果 DS18B20 完全不回应。温度值拼好之后显示就交给 LCD 了。一个典型显示格式是第一行显示「Temp:」第二行显示「25.3 C」。注意 1602 一个字符占一个字节sprintf 生成的字符串可能是变长的你需要控制位数比如固定%d.%d的格式避免整数部分变大后把第二行顶满。4.3 接入 DS1302 时钟显示三线协议的初始化和时间回读DS1302 是串行时钟芯片通过 RST、SCLK、I/O 三根线和单片机通信。例程里 DS1302 的驱动核心是把「写一个字节」和「读一个字节」的时序实现好初始化和时间读取都是在这两个函数上拼起来的。时间数据的格式是 BCD 码——比如 12 点 05 分寄存器里存的是 0x12 和 0x05不是 12 和 5。所以显示前要做 BCD 转 ASCIIunsigned char BCD_To_Dec(unsigned char bcd) { return (bcd 4) * 10 (bcd 0x0F); }这个转换在 LCD 显示时特别重要。直接在 LCD 上送 0x12 这个值显示的是 ASCII 码为 0x12 的字符通常是不可见控制符而不是「12」两个字符。例程里是把 BCD 拆成十位和个位分别加 0x30 变成 ASCII 码再送去显示。DS1302 还有一个常见操作是写入初始时间——每次重新上电后 DS1302 的时间会丢失除非你给它接了备用电池。例程里通常会在初始化时判断一个标志位比如写一个特定字节到某个 RAM 单元决定是否需要重新写时间。如果你不想重新写时间可以屏蔽掉写入那段只读取时间显示。注意 DS1302 的时间寄存器只有 7 个而且每个寄存器里有些位是无效的读取时先暂停时钟再连续读 7 个字节最后恢复时钟这个顺序乱不得。三个模块都接入后主循环的结构大概是按键扫描更新页面然后 switch(page) 分别调用温度刷新函数、时钟刷新函数或默认欢迎页。LCD 本身不刷屏只有对应页面的数据变化时才调用定位和写函数这样能避免字符重叠和闪烁。5. 常见问题与避坑LCD1602 调试中的五个典型翻车现场5.1 现象屏幕全黑或全亮没有任何字符上电后 LCD 背光亮但无字符或者整个屏幕是一个个实心方块。这通常不是程序逻辑问题而是引脚定义不对或对比度电压没调好。原因分析V0 电压决定字符清晰度接 GND 时对比度最高但过高会出现全部显示方块引脚 RS 和 E 映射错位初始化指令根本没送到 LCD 内部。解决办法先用螺丝刀调电位器让 V0 电压在 0.4-0.6V 之间然后用万用表量 RS、E 引脚的电平变化确认单片机确实输出了信号。最常见的是有人把 RS 和 E 的一根杜邦线接反了初始化序列全是废的。5.2 现象字符显示出来是乱的或者每隔几个字符就有杂音显示内容部分正确、部分乱码而且乱码位置固定。这个问题的根源一般是数据总线上的电平不稳。P0 口没接上拉电阻、数据线接触不良、或者杜邦线过长导致信号反射都会让某些位读成错误值。我记得有一次仿真怎么调都对焊到板子上就乱字最后发现是 20cm 长的杜邦线把 D3 那根信号搞得边缘太差。解决办法缩短数据线长度给 P0 加上 4.7K-10K 上拉还有注意 8 根数据线的顺序——有的人把 D0-D7 插反了Lcd_WriteCmd 发的 0x38 实际收到的是 0x1C完全是牛头不对马嘴。5.3 现象显示正常但光标乱跳或者字符自动滚动初始化完成、字符能显示了但光标位置不受控制第二行显示的内容跑到第一行去。这类问题通常是 Lcd_SetCursor 的列地址换算有误——如果你用的是 4 位模式DDRAM 地址是分两次发送的高位和低位的顺序不能反。另外如果初始化时用了 0x07写入后整体左移那么每写一个字符整个屏幕就会往左滚一格看起来就像光标乱跳。这个设置位和 0x06 只差一个 bit非常容易搞混——0x06 是光标右移整体不动0x07 是整体左移视觉差别极大。5.4 现象DS18B20 温度一直是 85 度或 0 度DS18B20 显示 85 是上电后的默认值说明单片机读到了数据但转换流程停留在初始状态显示 0 度则往往是读回来的数据全零。前者的原因一般是启动转换指令没拿到应答——恢复时序不对DS18B20 根本没进入转换流程后者通常是 DQ 引脚的外部上拉太弱读位时序里采样点落在低电平区间。解决办法确认 DQ 引脚接了 4.7K 上拉到 VCC然后检查初始化函数里的 480us 复位和 60us 等待应答是否精确特别是中断程序和时间片轮转会打断这些微秒级时序——调试时先关掉所有中断。5.5 现象DS1302 时间不走或回读全 FF时钟不走的原因大多是时钟停止位没清DS1302 的写保护寄存器 0x8E 要先写 0再写时间数据最后把写保护打开回读全是 0xFF 则说明通信时序有误通常是 SCLK 的拉高保持时间不够或者 RST 引脚的控制顺序错误——必须先把 RST 拉高再开始提供时钟脉冲操作结束后 RST 拉低。这个顺序反了DS1302 就认为不是一次有效的通信。例程里这几个控制语句是写好的但如果你修改了引脚映射要确认 sbit 定义没有写反我有一次把 SCLK 和 I/O 引脚定义对调了读出来所有字节都是 0xFF。提示以上五类问题覆盖了我遇到过的 80% 以上的 LCD1602 调试故障。核心排查顺序是硬件连接 → 电压 → 引脚定义 → 时序 → 逻辑。不要一上来就怀疑代码逻辑LCD 这种老接口硬件层面的低级失误概率远高于算法问题。6. 进阶技巧用自定义字模在 LCD1602 上显示特殊符号和汉字LCD1602 的标准字库只有 ASCII 字符但 HD44780 控制器还提供了 8 个可自定义的 CGRAM 字符位。你可以在 0x40-0x7F 的 CGRAM 地址区定义最多 8 个 5x8 点阵的自定义图案用来显示箭头、温度符号比如右上角的 ℃ 有小圆圈、进度条等。这个功能在例程里是有预留接口的很多初学者不知道。自定义字模的使用分三步。第一步规划好字模数据每个字符 8 个字节每字节只用低 5 位逐行定义点阵的亮灭。第二步把这 8 个字节写入 CGRAM。第三步在显示位置调用对应的字符码 0x00-0x07即可显示自定义图案。以温度符号 ℃ 为例它的右上角小圆圈就是自定义出来的// 自定义字模数据温度符号的上标圆圈 unsigned char code Custom_Char[8] { 0x0E, // 01110 0x11, // 10001 0x11, // 10001 0x0E, // 01110 0x00, // 00000 0x00, // 00000 0x00, // 00000 0x00 // 00000 }; void Lcd_WriteCustomChar(unsigned char addr, unsigned char *pattern) { unsigned char i; Lcd_WriteCmd(0x40 (addr * 8)); // CGRAM起始地址 0x40 字符编号*8 for(i 0; i 8; i) { Lcd_WriteData(pattern[i]); // 依次写入8行点阵数据 } }注意 CGRAM 的地址计算0x40 是 CGRAM 的起始基地址每个自定义字符占 8 个地址空间所以第 0 个字符的地址是 0x40第 1 个字符是 0x48以此类推。写完 CGRAM 后DDRAM 地址指针会自动停留在 CGRAM 区域内你必须重新发送一次定位指令比如Lcd_WriteCmd(0x80 | 0x00)才能回到 DDRAM 显示区否则后续的 Lcd_ShowString 会把数据写到 CGRAM 里屏幕上一片空白。这个「用完 CGRAM 必须切回 DDRAM」的细节是自定义字符最经典的翻车点。写入完 CGRAM后面就是普通显示。Lcd_SetCursor(1, 5); Lcd_WriteData(0x00); 就能在指定位置显示那个自定义的小圆圈。如果你把数字、字母做成了大字体的 2x2 拼接效果——在 CGRAM 里定义上半部分和下半部分一个字符拆成 4 个位置组合显示——整个屏幕的信息量能翻倍这也是很多人把 1602 玩出花来的进阶方向。从那以后我每次用 LCD1602 做项目都会强制自己先写好「初始化和引脚定义」这一层最基础的驱动再往上叠业务逻辑遇到调不通的情况先回头查物理层和时序层绝不去业务代码里瞎找。这套例程帮我把 LCD 从「黑匣子」变成了「透明设备」。如果你也被显示模块卡过希望这份拆解和避坑记录能帮你少走几段弯路。希望帮到你。本文还有配套的精品资源点击获取