
简介这份资源是面向C51单片机初学者与嵌入式爱好者的LCD1602显示例程合集围绕字符型液晶屏与8051内核单片机的交互展开覆盖矩阵按键键值、DS18B20温度、DS1302实时时钟以及ADC0832模数转换电压四类典型应用场景帮助读者打通显示驱动与常用外设通信的入门路径。压缩包共86个文件约4.13MB包含c源码、h头文件、uvproj与uvopt工程配置、hex烧录文件、dsn与pdsprj仿真工程、lst与m51编译列表以及mp4操作录屏兼顾代码实现与仿真演示。资源已有1076人学习下载每个功能模块均配有独立工程与对应视频读者可对照仿真过程理解RS、RW、E等控制信号的使用方式掌握单总线温度读取、串行时钟通信与8位模数转换的数据处理思路为后续电子项目开发积累可复用的代码框架与排错经验。1. 从一块“点不亮”的屏说起这套 C51 显示例程到底能跑出什么很多人第一次把 LCD1602 焊到开发板上通电后看到的不是字符而是一排方块或者干脆全黑于是开始怀疑是不是屏坏了。其实 LCD1602 本身不带字库也不会上电自动初始化它需要单片机按严格的时序把指令和数据“喂”进去才会显示出你想要的内容。这套 LCD1602C51 单片机的显示例程解决的就是这个从“点不亮”到“稳定显示”的过程并且把按键、DS18B20 温度采集、DS1302 时钟这几块常用外设串在一起形成一个可以实际运行的小系统。它适合刚接触 51 单片机、想找一个能直接编译下载、又能在此基础上加功能的人。例程里既有最基础的字符显示也有动态刷新和按键交互拿来当练手项目或者做课程设计的底子都挺合适。2. 硬件连接与工程结构先把线接对再谈代码2.1 引脚分配与最小系统LCD1602 一般有 16 个引脚实际用到的控制线就三根RS、RW、E数据线用 8 位或者 4 位模式。这套例程通常采用 8 位并行方式接在单片机的一个完整端口上比如 P0 或者 P2。RS 接一根普通 IO 用来区分指令和数据RW 接 IO 控制读写方向E 接 IO 产生使能脉冲。背光可以通过电位器调节对比度这个细节经常被忽略但对比度不对显示内容会淡到看不见或者黑成一片。DS18B20 是单总线器件只需要一根 IO 加一个上拉电阻就能读温度。DS1302 是三线制时钟芯片CE、IO、SCLK 三根线配合读写时序。按键部分一般用独立按键接在几个 IO 上按下时拉低电平。整个系统用 11.0592MHz 或 12MHz 晶振都可以前者对串口波特率更友好后者延时计算更直观。注意LCD1602 的背光和对比度是两回事背光只是让背景亮起来对比度决定字符深浅调不好会误判成程序问题。2.2 工程目录与文件划分一个能维护的 C51 工程不会把所有代码塞进一个 main.c。常见的做法是按功能拆成几个文件主程序只负责调度底层驱动各自独立。下面是一个典型的目录结构LCD1602_Demo/ ├── main.c // 主循环任务调度 ├── lcd1602.c // LCD1602 驱动 ├── lcd1602.h ├── key.c // 按键扫描 ├── key.h ├── ds18b20.c // 温度读取 ├── ds18b20.h ├── ds1302.c // 时钟读写 ├── ds1302.h └── delay.c // 延时函数这样分的好处是换一块单片机或者改引脚时只需要动对应的头文件和驱动里的宏定义主逻辑不用大改。Keil 里建工程时把这些 .c 文件都加进去头文件路径设好编译就能过。2.3 关键宏定义与引脚配置引脚配置一般写在头文件里用宏定义方便修改。比如// lcd1602.h #ifndef __LCD1602_H #define __LCD1602_H #include reg52.h #define LCD_DATA P0 // 数据口 sbit LCD_RS P2^6; // 寄存器选择 sbit LCD_RW P2^5; // 读写选择 sbit LCD_EN P2^7; // 使能信号 void LCD_Init(void); void LCD_ShowChar(unsigned char x, unsigned char y, char c); void LCD_ShowString(unsigned char x, unsigned char y, char *str); #endif这里把数据口和三个控制线都做成宏或位定义后面驱动里直接引用。如果实际接线换了端口只改这几行就行不用满工程搜 P0。参数说明x 是列坐标 0 到 15y 是行坐标 0 或 1超出范围会显示到奇怪的位置写的时候要自己保证不越界。3. 核心驱动拆解LCD1602 初始化、写指令与显示刷新3.1 初始化时序为什么不能省LCD1602 上电后内部状态是不确定的必须按数据手册的初始化序列走一遍。常见流程是延时等待内部复位然后连续写几次功能设置指令再关显示、清屏、开显示、设置输入模式。下面是一段典型的初始化代码// lcd1602.c #include lcd1602.h #include delay.h // 写指令 void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; // 选择指令寄存器 LCD_RW 0; // 写模式 LCD_DATA cmd; // 把指令放到数据口 LCD_EN 1; // 使能拉高 DelayUs(5); // 保持一段时间 LCD_EN 0; // 产生下降沿指令写入 DelayUs(100); // 等待指令执行 } // 写数据 void LCD_WriteData(unsigned char dat) { LCD_RS 1; // 选择数据寄存器 LCD_RW 0; LCD_DATA dat; LCD_EN 1; DelayUs(5); LCD_EN 0; DelayUs(100); } // 初始化 void LCD_Init(void) { DelayMs(15); // 上电等待 LCD_WriteCmd(0x38); // 8 位数据两行显示5x7 点阵 DelayMs(5); LCD_WriteCmd(0x38); DelayUs(100); LCD_WriteCmd(0x38); LCD_WriteCmd(0x0C); // 开显示关光标不闪烁 LCD_WriteCmd(0x06); // 写入后地址自动加 1 LCD_WriteCmd(0x01); // 清屏 DelayMs(2); // 清屏指令需要较长时间 }逻辑说明写指令和写数据的区别只在 RS 电平RW 固定为写。E 引脚需要一个高脉冲下降沿时 LCD 才真正读取数据口上的内容。初始化里连续写三次 0x38 是为了兼容不同批次的上电状态这是数据手册推荐的做法。参数 0x38 表示 8 位接口、两行显示、5x7 点阵0x0C 表示显示开、光标关0x06 表示地址指针自动加一写一个字符后光标右移。3.2 字符显示与坐标换算LCD1602 内部有两行显示 RAM第一行地址从 0x80 开始第二行从 0xC0 开始。要显示字符先写地址指令再写数据。封装成函数后调用者只需要给行列坐标void LCD_ShowChar(unsigned char x, unsigned char y, char c) { unsigned char addr; if (y 0) addr 0x80 x; // 第一行 else addr 0xC0 x; // 第二行 LCD_WriteCmd(addr); // 设置显示地址 LCD_WriteData(c); // 写入字符 } void LCD_ShowString(unsigned char x, unsigned char y, char *str) { while (*str ! \0) { LCD_ShowChar(x, y, *str); x; str; } }这里 x 的范围是 0 到 15y 是 0 或 1。如果 x 超过 15地址会跑到不可见区域屏幕上什么都不显示这是新手常踩的坑。字符串显示就是循环调用单字符函数每写一个字符列坐标加一。3.3 动态刷新与按键联动静态显示只是第一步实际项目里温度和时间都在变需要定时刷新。主循环里一般这样组织// main.c #include lcd1602.h #include ds18b20.h #include ds1302.h #include key.h void main(void) { int temp; unsigned char hour, min, sec; char buf[16]; LCD_Init(); while (1) { temp DS18B20_ReadTemp(); // 读温度 DS1302_ReadTime(hour, min, sec); // 读时间 // 第一行显示温度 sprintf(buf, Temp:%d C , temp / 10); LCD_ShowString(0, 0, buf); // 第二行显示时间 sprintf(buf, Time:%02d:%02d:%02d, hour, min, sec); LCD_ShowString(0, 1, buf); if (Key_Scan() 1) // 按键按下 { LCD_WriteCmd(0x01); // 清屏 LCD_ShowString(0, 0, Key Pressed); } } }逻辑说明主循环不断读取温度和时钟格式化后写到 LCD。sprintf 在 51 上比较耗资源如果空间紧张可以自己写数字转字符串函数。按键扫描返回键值按下时清屏并显示提示松开后下一轮循环又会刷新回温度和时间的显示。参数方面DS18B20 返回的温度通常放大 10 倍显示时除以 10 取整数部分。4. 外设接入DS18B20 温度与 DS1302 时钟的读写要点4.1 DS18B20 单总线时序DS18B20 对时序很敏感延时不准就会读回 85 或者全 0。复位、写位、读位都要按微秒级操作。下面是一段常见的读温度实现// ds18b20.c #include ds18b20.h #include delay.h sbit DQ P3^7; // 单总线数据线 // 复位 unsigned char DS18B20_Reset(void) { unsigned char presence; DQ 0; DelayUs(480); // 拉低至少 480us DQ 1; DelayUs(60); // 等待器件响应 presence DQ; // 读到 0 表示存在 DelayUs(420); return presence; } // 写一个字节 void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DQ 0; DelayUs(2); DQ dat 0x01; // 先写低位 DelayUs(60); DQ 1; dat 1; } } // 读温度 int DS18B20_ReadTemp(void) { unsigned char low, high; int temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过 ROM DS18B20_WriteByte(0x44); // 启动转换 DelayMs(750); // 等待转换完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 low DS18B20_ReadByte(); high DS18B20_ReadByte(); temp (high 8) | low; return temp; }逻辑说明复位时拉低 480 微秒以上然后释放并读取存在脉冲。写字节时每个位先拉低再根据数据位设置电平保持 60 微秒左右。读温度先发跳过 ROM 和启动转换指令等待 750 毫秒再发读暂存器指令读两个字节。返回的 temp 是原始值实际温度需要乘以 0.0625例程里为了显示方便通常做整数处理。4.2 DS1302 三线读写与时间格式DS1302 通过 CE、IO、SCLK 三根线通信数据在 SCLK 上升沿写入、下降沿读出。读写时要先发命令字节再传数据。下面是一个写寄存器的函数// ds1302.c #include ds1302.h #include delay.h sbit CE P3^4; sbit IO P3^5; sbit SCLK P3^6; // 写一个字节 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { IO dat 0x01; // 先发低位 SCLK 1; SCLK 0; dat 1; } } // 写寄存器 void DS1302_WriteReg(unsigned char cmd, unsigned char dat) { CE 0; SCLK 0; CE 1; DS1302_WriteByte(cmd); // 命令字节 DS1302_WriteByte(dat); // 数据字节 CE 0; }逻辑说明CE 拉高后开始通信先写命令字节再写数据字节最后 CE 拉低结束。命令字节里包含地址和读写方向位。读时间时先写读命令再读回数据注意 DS1302 返回的是 BCD 码显示前要转成十进制。参数方面秒寄存器的最高位是时钟停止位上电后要先清零才能走时。4.3 按键扫描与消抖按键部分比较简单但机械按键有抖动不处理会一次按下触发多次。常见做法是扫描到低电平后延时 10 到 20 毫秒再确认// key.c #include key.h #include delay.h sbit KEY1 P3^2; unsigned char Key_Scan(void) { if (KEY1 0) { DelayMs(15); // 消抖 if (KEY1 0) { while (KEY1 0); // 等待松开 return 1; } } return 0; }逻辑说明检测到低电平后延时再判断确认按下后等待松开再返回键值避免长按重复触发。如果要做长按功能可以在这里加计时逻辑。参数 15 毫秒是经验值抖动时间一般在 5 到 10 毫秒取 15 比较稳。5. 避坑与排查调不通时先看这几处5.1 屏幕全黑或者只有一排方块现象上电后 LCD 显示一排黑色方块或者整屏黑调对比度也没用。 原因初始化没有成功或者 E 引脚没有产生下降沿LCD 一直处于未初始化状态。 解决先用示波器或者逻辑分析仪看 E 引脚有没有脉冲没有就检查代码里 E 的拉高拉低顺序。确认初始化里延时足够尤其是上电后的 15 毫秒等待不能省。对比度电位器先调到中间位置再微调。5.2 显示内容乱码或者位置错乱现象字符显示出来了但位置不对或者出现乱码。 原因地址设置错误或者数据口接线顺序反了。比如 P0 口接 LCD 数据线时P0.0 对应 DB0如果接反了写进去的字节位序就乱了。 解决检查数据线是否按位对应地址计算里第一行 0x80 加列号、第二行 0xC0 加列号有没有写错。字符串显示时注意不要越界x 超过 15 会跑到不可见区域。5.3 DS18B20 一直返回 85 或者 0现象温度显示固定 85 或者 0不随环境变化。 原因单总线时序不对复位没有检测到存在脉冲或者转换等待时间不够。 解决先确认 DQ 线上有 4.7k 上拉电阻。用示波器看复位时拉低时间是否达到 480 微秒。读温度时启动转换后至少等 750 毫秒等待时间不够会读到上一次的结果或者默认值 85。5.4 DS1302 时间不走或者读回乱码现象时间显示不变或者读回来的数字明显不对。 原因DS1302 的秒寄存器停止位没有清零或者 BCD 码没有转换。 解决初始化时先写秒寄存器把最高位清零启动走时。读回的小时、分钟、秒都是 BCD 格式显示前要转成十进制比如 0x23 要转成 23。检查 CE、SCLK、IO 三根线的接线和时序SCLK 空闲时保持低电平。5.5 按键一次按下触发多次现象按一下按键屏幕刷新了好几次或者功能跳了好几格。 原因机械按键抖动没有处理或者等待松开的逻辑缺失。 解决在检测到低电平后加 10 到 20 毫秒延时再确认确认后等待按键松开再返回。如果按键接在中断引脚上还要考虑中断触发方式下降沿触发比低电平触发更稳。6. 二次开发把这套例程改成自己的项目拿到这套例程后最直接的改法是换显示内容。比如把温度和时间换成传感器数值、菜单选项或者状态提示。改的时候先动 main.c 里的格式化字符串再根据需要调整 LCD_ShowString 的坐标。如果显示内容超过 16 个字符要么滚动显示要么分屏切换LCD1602 一行只能放 16 个字符这是硬件限制。第二个改法是增加外设。比如加一个蜂鸣器做报警加一个继电器做控制。这些都是在主循环里加判断逻辑读按键或者读传感器满足条件就操作对应 IO。注意 51 的 IO 驱动能力有限驱动继电器要加三极管或者光耦不要直接接。第三个改法是优化刷新逻辑。现在的主循环是全速刷新LCD 和传感器都在不停读写功耗和响应都不太理想。可以改成定时器中断里置标志位主循环检测到标志再刷新比如 200 毫秒刷一次。这样按键响应更及时也不会因为 LCD 写操作太频繁导致闪烁。下面是一个用定时器控制刷新节奏的示例// main.c 片段 volatile unsigned char refresh_flag 0; void Timer0_ISR(void) interrupt 1 { static unsigned int cnt 0; TH0 0xFC; // 1ms 定时初值12MHz TL0 0x18; cnt; if (cnt 200) // 200ms { cnt 0; refresh_flag 1; // 置刷新标志 } } void main(void) { LCD_Init(); Timer0_Init(); // 初始化定时器 while (1) { if (refresh_flag) { refresh_flag 0; // 这里做 LCD 刷新和传感器读取 } // 按键扫描可以放在这里不受刷新节奏影响 } }逻辑说明定时器每 1 毫秒中断一次累计 200 次置一次刷新标志。主循环只在标志置位时做 LCD 刷新其他时间处理按键。这样刷新频率稳定也不会因为传感器读取耗时导致按键卡顿。参数方面12MHz 晶振下 1 毫秒定时初值是 0xFC1811.0592MHz 下需要重新计算。改的时候还有一点要注意sprintf 在 51 上占用不少 ROM 和 RAM如果工程空间紧张可以自己写一个简单的数字转字符串函数只处理整数和固定格式。另外LCD1602 的写操作之间要留足延时尤其是清屏指令需要 1.5 毫秒以上延时不够会丢指令。从那以后我每次改显示类项目都会先把刷新节奏和按键扫描分开再动显示内容不然调着调着就乱成一锅粥。希望帮到你。本文还有配套的精品资源点击获取