简介:这是一套面向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. 从一块“点不亮”的屏说起:LCD1602+C51 显示例程能帮你省下多少调试时间
如果你手头正好有一块 LCD1602 液晶屏、一片 C51 内核的单片机,外加 DS18B20 和 DS1302 这类常用外设,大概率会遇到同一个场景:硬件连线检查了三遍,代码烧进去,屏幕要么全亮蓝底、要么只显示一排方块,调对比度电位器调到手指发酸也没用。这套 LCD1602+C51 显示例程就是冲着这个场景来的——它把字符屏驱动、按键扫描、温度采集、时钟读写这几件事打包成一个可以直接编译烧录的工程,适合刚接触 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; // RS=0 表示写命令 LCD_RW = 0; // RW=0 表示写操作 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; // RS=1 表示写数据 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,那是器件上电默认值,说明通信根本没成功。
温度转换流程是:复位、跳过 ROM(0xCC)、启动转换(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 固定为 1,bit6 是 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 或 -127
85.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 区,再决定用哪种方式。希望帮到你。
本文还有配套的精品资源,点击获取