
简介这是一套面向C51单片机初学者与嵌入式爱好者的LCD1602显示例程合集围绕字符型液晶屏与8051内核单片机的交互展开覆盖矩阵按键键值读取、DS18B20温度采集、DS1302实时时钟以及ADC0832模数转换电压显示四类典型应用场景适合课程设计、电子实训与入门项目参考。资源包共86个文件约4.13MB包含c源码、h头文件、uvproj与uvopt工程配置、hex烧录文件、dsn与pdsprj仿真工程以及mp4操作演示视频另附lst、m51等编译中间文件便于对照调试。目前已有1076人学习下载。四个独立工程分别演示按键扫描、单总线测温、串行时钟读取与模拟量采集的完整实现配合仿真视频可直观观察LCD1602的RS、RW、E信号时序与数据显示效果帮助读者快速理解外设驱动编写与硬件接口调试思路。1. 从一块“点不亮”的屏说起LCD1602C51 显示例程能帮你省下多少调试时间如果你手头正好有一块 LCD1602 液晶屏、一片 C51 内核的单片机外加 DS18B20 和 DS1302 这类常用外设大概率会遇到同一个场景硬件连线检查了三遍代码烧进去屏幕要么全亮蓝底、要么只显示一排方块调对比度电位器调到手指发酸也没用。这套 LCD1602C51 显示例程就是冲着这个场景来的——它把字符屏驱动、按键扫描、温度采集、时钟读写这几件事打包成一个可以直接编译烧录的工程适合刚接触 51 单片机外设驱动的新手也适合想拿一套稳定底层做二次开发的老手。例程覆盖了 LCD1602 的 4 位/8 位并口驱动、DS18B20 单总线时序、DS1302 三线串行时钟以及独立按键的消抖处理基本把入门阶段最常打交道的几个模块都串起来了。2. 工程结构与环境搭建从 Keil 工程到最小系统接线2.1 目录结构与文件职责拿到一份 C51 例程第一件事不是急着打开 main.c而是先看目录怎么分的。这套例程的典型结构大致如下LCD1602_C51_Demo/ ├── main.c // 主循环调度显示、按键、采集 ├── lcd1602.c/.h // 液晶底层驱动写命令/写数据/忙检测 ├── ds18b20.c/.h // 单总线温度传感器时序 ├── ds1302.c/.h // 三线串行时钟芯片读写 ├── key.c/.h // 独立按键扫描与消抖 ├── delay.c/.h // 微秒/毫秒级延时 └── Project.uvproj // Keil uVision 工程文件这种按外设拆文件的写法在 51 项目里很常见好处是换一块单片机或者换一个引脚定义时只需要改对应的 .h 和初始化函数不用在几千行的 main.c 里翻找。我一般会额外加一个config.h把所有引脚定义、晶振频率、是否启用某个外设的宏集中在一起后面移植会省很多事。2.2 Keil 工程配置与编译选项用 Keil uVision 打开工程后重点检查三个地方。第一是 Device 选型必须和你手上的芯片型号一致比如 STC89C52 或 AT89C51不同型号的 RAM 大小和定时器数量有差异。第二是 Output 选项卡里勾选 “Create HEX File”否则烧录器找不到可执行文件。第三是 Target 里的晶振频率例程延时函数通常按 11.0592MHz 或 12MHz 写的填错会导致 DS18B20 时序偏差读出来的温度要么是 85 要么是 -127。// config.h 里集中定义避免到处改 #define FOSC 11059200L // 晶振频率影响延时精度 #define LCD_DATA P0 // LCD 数据口 #define LCD_RS P2_6 // 寄存器选择 #define LCD_RW P2_5 // 读写选择 #define LCD_EN P2_7 // 使能信号 #define DS18B20_PIN P3_7 // 单总线数据脚 #define DS1302_IO P3_4 // 时钟数据脚这里FOSC的单位是 Hz延时函数里用while(--i)空循环实现微秒级延时时循环次数就是根据这个值算出来的。如果你换成 12MHz 晶振DS18B20 的复位脉冲宽度会差出几个微秒虽然单总线协议有一定容错但批量生产时容易出玄学问题。2.3 最小系统接线与上电检查接线之前先确认单片机最小系统能跑起来电源 5V、地、晶振加两个 30pF 电容、复位电路一个 10uF 电容加 10K 电阻。LCD1602 这边VSS 接地、VDD 接 5V、V0 接一个 10K 电位器中间脚用来调对比度背光 A/K 根据模块型号接限流电阻。数据口用 4 位模式的话只接 D4-D7能省 4 根 IO。上电后如果屏幕第一行显示一排黑方块说明 LCD 初始化基本成功只是没写数据如果全屏蓝底没字符先调 V0 电位器再检查 RS/RW/EN 三根控制线有没有接反。DS1302 需要外接 32.768kHz 晶振和一颗纽扣电池否则掉电后时间不保存。DS18B20 的数据脚要接一个 4.7K 上拉电阻到 5V这个电阻漏掉的话温度读数会随机跳变。3. 核心模块拆解LCD1602 驱动、DS18B20 时序与 DS1302 读写3.1 LCD1602 写命令与写数据LCD1602 的驱动核心就两个函数写命令和写数据。写命令用来设置显示模式、光标位置、清屏等写数据用来往 DDRAM 里填字符。4 位模式下一个字节分两次发送先高四位再低四位。// lcd1602.c void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; // RS0 表示写命令 LCD_RW 0; // RW0 表示写操作 LCD_DATA cmd 4; // 先发高四位 LCD_EN 1; delay_us(20); LCD_EN 0; // 使能脉冲下降沿锁存 delay_us(20); LCD_DATA cmd 0x0F; // 再发低四位 LCD_EN 1; delay_us(20); LCD_EN 0; delay_ms(2); // 命令执行需要时间清屏等长命令要更久 } void LCD_WriteData(unsigned char dat) { LCD_RS 1; // RS1 表示写数据 LCD_RW 0; LCD_DATA dat 4; LCD_EN 1; delay_us(20); LCD_EN 0; delay_us(20); LCD_DATA dat 0x0F; LCD_EN 1; delay_us(20); LCD_EN 0; delay_ms(1); }LCD_RS决定写的是命令还是数据LCD_RW在大多数只写应用里可以直接接地省一根 IO。LCD_EN的下降沿是数据锁存时刻所以每次发完四位都要拉高再拉低。delay_ms(2)是给清屏、归位这类慢命令留的执行时间如果你发现屏幕偶尔花屏多半是这个延时不够。初始化流程一般是延时 15ms 等内部复位、写 0x38 设置 4 位两行显示、写 0x0C 开显示关光标、写 0x06 设置写入后光标右移、最后写 0x01 清屏。顺序不能乱尤其是 0x38 必须在前几次发送中重复两遍这是 LCD1602 上电后的固定套路。3.2 DS18B20 复位、写字节与读字节DS18B20 是单总线器件所有通信靠一根线完成时序要求比较严格。核心操作有三个复位、写一个字节、读一个字节。// ds18b20.c unsigned char DS18B20_Reset(void) { unsigned char presence; DS18B20_PIN 0; // 拉低总线 delay_us(480); // 保持至少 480us DS18B20_PIN 1; // 释放总线 delay_us(60); // 等待器件拉低回应 presence DS18B20_PIN; // 读到 0 表示器件存在 delay_us(420); // 等复位时序结束 return presence; } void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS18B20_PIN 0; delay_us(2); DS18B20_PIN dat 0x01; // 写 1 释放写 0 继续拉低 delay_us(60); DS18B20_PIN 1; // 恢复总线 dat 1; } } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { DS18B20_PIN 0; delay_us(2); DS18B20_PIN 1; // 释放总线准备读 delay_us(2); dat 1; if (DS18B20_PIN) dat | 0x80; // 先读低位拼到高位 delay_us(60); } return dat; }复位时拉低 480us 是协议规定的短了器件不响应长了也没事但浪费时间。写字节时每个 bit 的窗口是 60us 左右写 0 就是整个窗口都拉低写 1 是拉低 2us 后释放。读字节时主机拉低 2us 后释放然后在 15us 内采样总线电平。这些延时都是微秒级如果晶振频率和延时函数不匹配读出来的温度会固定在 85.0那是器件上电默认值说明通信根本没成功。温度转换流程是复位、跳过 ROM0xCC、启动转换0x44、延时 750ms、复位、跳过 ROM、读暂存器0xBE、读两个字节、拼成 16 位、乘以 0.0625 得到摄氏度。负温度要判断高字节符号位取反加一后再乘。3.3 DS1302 三线串行读写与时间 BCD 码DS1302 用 CE、IO、SCLK 三根线通信数据是 BCD 码格式。写一个字节是低位在前读也是低位在前。// ds1302.c void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_IO dat 0x01; // 先发低位 DS1302_SCLK 1; delay_us(2); DS1302_SCLK 0; // 上升沿写入 dat 1; } } unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; if (DS1302_IO) dat | 0x80; // 先读低位拼到高位 DS1302_SCLK 1; delay_us(2); DS1302_SCLK 0; // 下降沿读出 } return dat; }写寄存器时要先发命令字节命令字节的 bit7 固定为 1bit6 是 RAM/时钟选择bit5-1 是寄存器地址bit0 是读写位。比如写秒寄存器是 0x80读秒寄存器是 0x81。BCD 码转换就是(dat 4) * 10 (dat 0x0F)反过来是((val / 10) 4) | (val % 10)。初始化时记得先关写保护写 0x8E 寄存器数据 0x00 解除保护设置完时间后再写 0x80 打开保护。如果发现时间不走先检查 32.768kHz 晶振有没有起振用示波器看 DS1302 的 pin2 和 pin3正常应该有正弦波。纽扣电池电压低于 2V 时掉电后时间也会丢。4. 主循环与按键交互把显示、采集、时钟串起来4.1 主函数调度逻辑主循环不需要太复杂51 单片机资源有限轮询方式最稳。典型结构是初始化各外设、进入 while(1)、每隔一定时间读一次温度和时间、刷新 LCD、扫描按键。// main.c void main(void) { unsigned int temp; unsigned char sec, min, hour; LCD_Init(); DS1302_Init(); LCD_ShowString(0, 0, Temp: C); LCD_ShowString(1, 0, Time: : : ); while (1) { if (DS18B20_Reset()) { // 器件存在才读 DS18B20_WriteByte(0xCC); // 跳过 ROM DS18B20_WriteByte(0x44); // 启动转换 delay_ms(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 temp DS18B20_ReadByte(); temp | DS18B20_ReadByte() 8; temp temp * 0.0625 * 10; // 放大 10 倍保留一位小数 LCD_ShowNum(0, 6, temp / 10, 2); LCD_ShowChar(0, 8, .); LCD_ShowChar(0, 9, temp % 10 0); } DS1302_ReadTime(hour, min, sec); LCD_ShowNum(1, 6, hour, 2); LCD_ShowNum(1, 9, min, 2); LCD_ShowNum(1, 12, sec, 2); Key_Scan(); // 按键处理可切换页面或调整时间 delay_ms(200); // 刷新间隔太快会闪 } }DS18B20_Reset()的返回值用来判断器件是否在线如果返回 1 说明没检测到直接跳过读取避免程序卡在等待回应。温度放大 10 倍是为了在 LCD 上显示一位小数temp / 10是整数部分temp % 10是小数部分。DS1302 读时间时注意先读秒、再读分、再读时有些例程会读两次秒来避免进位误差这套例程里没做要求高的话可以补上。4.2 按键消抖与页面切换独立按键接在 P3 口按下时接地松开时靠上拉电阻拉高。机械按键抖动时间大概 10-20ms所以扫描时要先延时再确认。// key.c unsigned char Key_Scan(void) { static unsigned char key_up 1; // 按键松开标志 if (key_up (KEY1 0 || KEY2 0 || KEY3 0)) { delay_ms(10); // 消抖 key_up 0; if (KEY1 0) return 1; if (KEY2 0) return 2; if (KEY3 0) return 3; } else if (KEY1 1 KEY2 1 KEY3 1) { key_up 1; // 全部松开才允许下次触发 } return 0; }key_up这个静态变量是关键没有它的话按住按键会连续触发。返回 0 表示无按键返回 1/2/3 对应三个按键。主循环里根据返回值切换显示页面比如按 KEY1 进入时间设置模式按 KEY2 调整小时按 KEY3 调整分钟。设置模式下要记得关掉 DS1302 的写保护改完再打开。4.3 显示刷新与自定义字符LCD1602 内置了 192 个常用字符但显示温度单位 “°C” 里的度符号不在标准字库里需要用 CGRAM 自定义。CGRAM 有 8 个自定义字符位置地址是 0x40-0x7F每个字符占 8 个字节。// 自定义度符号存入 CGRAM 第 0 个位置 unsigned char code degree[8] { 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00, 0x00 }; void LCD_LoadCustomChar(void) { unsigned char i; LCD_WriteCmd(0x40); // CGRAM 起始地址 for (i 0; i 8; i) { LCD_WriteData(degree[i]); } }写完之后用LCD_WriteData(0)就能显示这个自定义字符。刷新显示时不要每次都清屏清屏命令太慢会导致肉眼可见的闪烁。正确做法是定位光标到固定位置只覆盖变化的数字位。比如温度从 25.1 变成 25.2只改最后一位就行。5. 避坑与常见问题排查从花屏到温度 85 的解决思路5.1 屏幕花屏或只显示一排方块现象是上电后 LCD 第一行出现一排黑色方块第二行空白写数据没反应。原因通常是初始化时序不对尤其是 4 位模式下第一次发送 0x38 之前没有足够延时。LCD1602 内部复位需要 15ms 以上有些模块甚至要 40ms。解决办法是在 LCD_Init 最开头加delay_ms(50)然后把 0x38 写两遍中间隔 5ms。另外检查 V0 电位器对比度太低也会看不到字符但方块是能看到的。5.2 DS18B20 读数固定为 85.0 或 -12785.0 是 DS18B20 上电默认的温度寄存器值说明主机根本没有成功启动转换。原因有三个数据脚没有接 4.7K 上拉电阻、延时函数不匹配导致复位脉冲宽度不对、或者器件供电不足。先量数据脚在空闲时是不是高电平如果不是检查上拉电阻。再用示波器看复位时拉低时间是不是接近 480us偏差超过 20% 就要改延时循环次数。如果读出来是 -127说明读到的两个字节都是 0xFF通常是读时序里采样点太晚把delay_us(2)改成delay_us(1)试试。5.3 DS1302 时间不走或掉电丢失时间不走先看晶振DS1302 的 32.768kHz 晶振起振很慢有些便宜晶振要几秒钟。如果一直不起振换一个晶振或者并联一个 6pF 电容。掉电丢失检查纽扣电池电压低于 2V 就换。还有一个容易忽略的点写时间之前必须解除写保护写 0x8E 寄存器数据 0x00设置完再写 0x80 恢复保护。如果忘了这一步时间写不进去读出来永远是初始值。5.4 按键反应迟钝或连击按键反应迟钝通常是消抖延时太长10ms 消抖加 200ms 主循环刷新按一下要等很久才响应。把主循环里的delay_ms(200)改成 50ms或者把按键扫描放到定时器中断里主循环只负责显示。连击问题就是key_up标志没处理好确保所有按键都松开后才置 1。如果按键接在 P0 口记得外接上拉电阻P0 口内部没有上拉。5.5 编译通过但烧录后没反应先确认 HEX 文件有没有生成Keil 的 Output 选项卡里 “Create HEX File” 必须勾选。然后检查烧录器的晶振频率设置和实际板子是否一致有些烧录软件默认 12MHz板子上是 11.0592MHz烧进去延时全乱。最后看复位电路51 单片机高电平复位如果复位脚一直拉高程序永远在复位状态。用万用表量复位脚电压正常工作时应该是 0V 左右。6. 二次开发改造从固定显示到多级菜单与数据记录这套例程跑通之后最自然的改造方向是加一个多级菜单把温度、时间、阈值设置都放到按键操作里。我一般会用一个状态机来管理页面每个页面有自己的按键处理函数和显示刷新函数。// menu.c 状态机框架 typedef enum { PAGE_MAIN, // 主页面显示温度和时间 PAGE_SET_HOUR, // 设置小时 PAGE_SET_MIN, // 设置分钟 PAGE_SET_ALARM // 设置温度报警阈值 } PageState; PageState page PAGE_MAIN; void Menu_Process(unsigned char key) { switch (page) { case PAGE_MAIN: if (key 1) page PAGE_SET_HOUR; break; case PAGE_SET_HOUR: if (key 2) DS1302_IncHour(); if (key 1) page PAGE_SET_MIN; break; case PAGE_SET_MIN: if (key 2) DS1302_IncMin(); if (key 1) page PAGE_SET_ALARM; break; case PAGE_SET_ALARM: if (key 2) alarm_temp; if (key 1) page PAGE_MAIN; break; } }这个状态机的好处是每加一个页面只需要加一个枚举值和对应的 case 分支不会把主循环撑爆。显示刷新根据page变量决定显示什么内容设置模式下让光标闪烁提示当前编辑位。另一个改造点是数据记录。DS1302 内部有 31 字节的 RAM可以用来存最近几条温度记录掉电不丢。写 RAM 的命令字节和写时钟寄存器类似只是地址从 0xC0 开始。每次读温度后把值存进去主页面加一个按键翻看历史记录。注意 DS1302 的 RAM 也是 BCD 码格式存之前要转换。如果你用的是 STC 系列单片机还可以把数据写到内部 EEPROM 里容量比 DS1302 的 31 字节大得多。STC89C52 有 2KB 的 EEPROM按扇区擦写存几千条记录没问题。改造时把ds1302.c里的 RAM 读写函数换成 EEPROM 操作函数就行上层逻辑不用动。最后说一个我踩过的坑改显示内容时不要直接改LCD_ShowString的字符串常量那些常量存在 code 区改多了会占满 Flash。我一般把要显示的字符串放到一个char数组里用sprintf格式化后再显示虽然多占一点 RAM但改起来灵活得多。从那以后我每次改显示逻辑都先确认字符串是存在 RAM 还是 code 区再决定用哪种方式。希望帮到你。本文还有配套的精品资源点击获取