☰
C51裸机实战:LCD1602驱动、DS18B20与DS1302集成例程
2026/10/21 2:09:35 网站建设 项目流程

简介:这是一套面向C51单片机初学者与嵌入式爱好者的LCD1602显示例程合集,围绕字符型液晶屏与8051内核单片机的交互展开,覆盖矩阵按键键值读取、DS18B20温度采集、DS1302实时时钟以及ADC0832模数转换电压显示四类典型应用场景,适合作为课程设计、电子实训或自学练手的参考素材。资源包共86个文件,约4.13MB,包含c源码、h头文件、a51启动代码、uvproj与uvopt工程配置、hex烧录文件、dsn与pdsprj仿真工程,以及mp4操作演示视频,工程与仿真文件配套齐全,便于直接打开复现。目前已有1076人学习下载。四个独立工程分别对应按键、温度、时钟与电压采集,代码中涵盖RS、RW、E控制信号与单总线、串行接口的驱动逻辑,配合仿真视频可直观对照硬件时序与显示效果,帮助读者理解外设通信协议与显示刷新流程,为后续扩展传感器项目积累可复用的驱动思路。

1. 一块 LCD1602 怎么把 C51 的 GPIO 榨干:从点亮到按键调时的完整例程

很多人第一次把 LCD1602 插上面包板,照着某份例程烧进去,屏幕要么全亮一排黑块,要么只亮背光不显字,于是开始怀疑是不是单片机坏了。其实 LCD1602 本身不复杂,它就是一个内置 HD44780 兼容控制器的字符屏,真正让人翻车的是初始化时序、忙标志判断和 4 位/8 位模式的接线差异。这份 C51 显示例程把 LCD1602 驱动、按键扫描、DS18B20 温度读取和 DS1302 时钟读写揉在一个工程里,适合正在做课程设计或想找一份能直接改的 51 裸机模板的人。它解决的不是“显示一行字”这种单点问题,而是把显示、输入、传感、计时四条线在一个主循环里跑通,让你拿到手就能往自己的板子上搬。

2. LCD1602 驱动层:4 位模式接线与初始化时序

2.1 为什么例程默认走 4 位模式

8 位模式一次把 8 根数据线全占掉,P0 或 P2 基本就没了,再挂 DS18B20 和 DS1302 就没引脚可用。4 位模式只用 D4~D7 四根线传数据,一个字节分两次送,先高四位再低四位。代价是驱动函数里多两次使能脉冲,但省下四根 IO,对 51 这种引脚紧张的平台是划算的。例程里一般把数据口定义成一个宏,方便你换到别的端口:

#include <reg52.h> #define LCD_DATA P0 // 4位模式下只用 P0.4~P0.7 sbit LCD_RS = P2^0; // 寄存器选择:0 指令,1 数据 sbit LCD_RW = P2^1; // 读写选择:0 写,1 读 sbit LCD_EN = P2^2; // 使能,下降沿锁存 // 微秒级延时,11.0592MHz 晶振下大约 1ms 量级靠循环凑 void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 110; j++); }

这里LCD_DATA只用了高四位,低四位悬空或接别的外设都行,但注意别让低四位在写操作时被意外拉低造成干扰。RS、RW、EN三根控制线随便挑三个空闲 IO,例程放在 P2 口是为了和 P0 数据口分开,布线清爽。

2.2 写指令和写数据的底层函数

4 位模式的核心是“高四位先走,低四位后走”,每次都要给 EN 一个高脉冲再拉低。下面这个lcd_write_byte是整份例程里最该看懂的函数:

// rs=0 写指令,rs=1 写数据 void lcd_write_byte(unsigned char dat, unsigned char rs) { LCD_RS = rs; LCD_RW = 0; // 只写不读,省掉忙标志判断 LCD_DATA = (LCD_DATA & 0x0F) | (dat & 0xF0); // 送高四位 LCD_EN = 1; delay_ms(1); LCD_EN = 0; LCD_DATA = (LCD_DATA & 0x0F) | ((dat << 4) & 0xF0); // 送低四位 LCD_EN = 1; delay_ms(1); LCD_EN = 0; }

逻辑说明:先保留 P0 低四位不变,把dat的高四位贴到 P0 高四位上,发一次使能;再把dat左移四位,让原来的低四位跑到高四位位置,再发一次使能。参数rs决定这次是命令还是数据。这里用delay_ms(1)代替读忙标志,是因为很多最小系统板把 RW 直接接地了,读不了忙标志,干脆用延时硬等。代价是刷屏慢一点,但 1602 本身刷新需求不高,够用。

提示:如果你的板子 RW 接了 IO 且想提速,可以把LCD_RW=1后读 P0 高四位判断 bit7 是否为 0,忙就循环等,这样能省掉固定延时。

2.3 初始化序列不能省的那几条指令

HD44780 上电后内部状态是乱的,必须按固定顺序发指令。4 位模式下第一步尤其关键,要先强制进入 4 位模式:

void lcd_init(void) { delay_ms(15); // 上电等待,别省 lcd_write_byte(0x33, 0); // 强制复位到 8 位模式 delay_ms(5); lcd_write_byte(0x32, 0); // 切到 4 位模式 delay_ms(5); lcd_write_byte(0x28, 0); // 4位总线,2行显示,5x7点阵 lcd_write_byte(0x0C, 0); // 开显示,关光标,不闪烁 lcd_write_byte(0x06, 0); // 写入后地址自动加1,画面不移动 lcd_write_byte(0x01, 0); // 清屏 delay_ms(2); // 清屏指令耗时较长 }

0x33和0x32这两条是 4 位模式专属的“唤醒”动作,少了它们屏幕大概率只显示一排黑块。0x28设定总线宽度和行数,0x0C控制显示开关,0x06决定写一个字符后光标往哪走。清屏0x01之后必须给足 2ms,否则紧接着写数据会丢。

3. 主循环怎么把按键、DS18B20、DS1302 串起来

3.1 按键扫描与显示刷新的节拍分配

主循环里最忌讳的是用delay_ms(500)这种长延时去等刷新,按键会变得迟钝。例程一般用一个软件计数分频:每轮循环扫一次按键,累计到一定次数再刷一次屏。这样按键响应在几十毫秒级,显示也不会闪。

unsigned char key_scan(void) { unsigned char key = 0; if (KEY1 == 0) { delay_ms(10); if (KEY1 == 0) key = 1; } // 消抖 if (KEY2 == 0) { delay_ms(10); if (KEY2 == 0) key = 2; } if (KEY3 == 0) { delay_ms(10); if (KEY3 == 0) key = 3; } return key; } void main(void) { unsigned char tick = 0; lcd_init(); ds1302_init(); while (1) { unsigned char k = key_scan(); if (k == 1) mode++; // 切换显示/设置模式 if (k == 2) value_adjust(); // 当前项加 if (k == 3) value_adjust_dec(); if (++tick >= 20) { // 约 200ms 刷一次 tick = 0; display_update(); } } }

key_scan里每个按键都做了 10ms 消抖,机械按键不消抖会一次按下触发好几次。tick分频让显示刷新和按键扫描解耦,这是裸机多任务最朴素也最稳的做法。mode变量决定当前屏幕显示的是温度还是时间,以及是否处于设置状态。

3.2 DS18B20 读取:时序比协议更重要

DS18B20 是单总线器件,对延时极其敏感,51 上跑它最容易翻车的地方就是延时不准。例程里通常把读写时隙写成独立函数:

void ds18b20_reset(void) { DQ = 0; delay_us(480); // 拉低至少 480us DQ = 1; delay_us(60); // 释放,等器件应答 // 此处可读 DQ 判断是否存在,例程为简化略过 delay_us(420); } void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; delay_us(2); DQ = dat & 0x01; // 先写低位 delay_us(60); DQ = 1; dat >>= 1; } }

delay_us的精度直接决定成败。11.0592MHz 下如果用定时器做微秒延时最稳,用空循环凑的话要拿示波器或逻辑分析仪校一下。写时隙里“拉低 2us 后放数据,再保持 60us”是标准节奏,写 0 和写 1 的区别就在这 60us 内 DQ 是高还是低。读时隙同理,拉低 2us 后释放,在 15us 内采样。

3.3 DS1302 读写与显示格式化

DS1302 是三线 SPI 类接口,CE、IO、SCLK 三根线。它内部有秒、分、时、日、月、周、年寄存器,数据是 BCD 码。例程里一般封装成“读一个字节/写一个字节”,再往上拼成时间结构体:

void ds1302_write_byte(unsigned char cmd, unsigned char dat) { unsigned char i; CE = 0; SCLK = 0; CE = 1; for (i = 0; i < 8; i++) { // 先发命令 IO = cmd & 0x01; SCLK = 1; SCLK = 0; cmd >>= 1; } for (i = 0; i < 8; i++) { // 再发数据 IO = dat & 0x01; SCLK = 1; SCLK = 0; dat >>= 1; } CE = 0; }

注意 DS1302 是低位先发,和很多 SPI 器件相反。命令字节里 bit0 是读写位,写寄存器用 0x80 起,读用 0x81 起。BCD 码转十进制显示时别忘了(dat >> 4) * 10 + (dat & 0x0F),直接当二进制打印会得到莫名其妙的时间。

显示格式化就是把温度和时间拼成字符串写到 1602 指定地址:

void display_update(void) { lcd_write_byte(0x80, 0); // 第一行首地址 lcd_show_string("Temp:"); lcd_show_num(temp_int); lcd_show_string("C"); lcd_write_byte(0xC0, 0); // 第二行首地址 lcd_show_string("Time:"); lcd_show_num(hour); lcd_show_string(":"); lcd_show_num(minute); }

0x80和0xC0分别是两行的起始 DDRAM 地址,写字符前先定位,否则会接着上次位置乱跑。

4. 避坑与排查:屏幕不亮、温度跳变、时间不走

4.1 上电只显示一排黑块

现象:背光亮,屏幕第一行全是实心方块,写什么指令都没反应。 原因:初始化序列没走完,或者 4 位模式没切换成功,控制器还停在 8 位模式等数据。 解决:确认0x33、0x32两条指令确实发出去了,且上电延时不少于 15ms。用示波器看 EN 脚有没有两次脉冲,没有就是卡在延时或端口定义错了。

4.2 显示字符缺笔画或错位

现象:能显示,但某个字符少一横,或者整体右移一格。 原因:4 位模式下高低四位拼接顺序反了,或者数据口低四位被别的外设干扰。 解决:检查lcd_write_byte里先送dat & 0xF0还是先送(dat << 4) & 0xF0,顺序反了字符就散。数据口低四位如果接了别的器件,写操作时先读回再改高四位,别直接赋值整个 P0。

4.3 DS18B20 读数一直是 85 或 0

现象:温度显示固定 85.0 或 0.0,怎么改都不动。 原因:85 是 DS18B20 上电默认值,说明转换命令没发成功或没等够转换时间;0 多半是复位时隙没检测到器件。 解决:确认复位后 DQ 被器件拉低过(存在脉冲),转换命令0x44发出后至少等 750ms 再读暂存器。延时函数用定时器重写一遍,空循环在优化等级变化时会飘。

4.4 DS1302 时间不走或走乱

现象:读出来时间不变,或者秒跳得飞快。 原因:晶振没起振,或者写保护位没解除。 解决:DS1302 的秒寄存器 bit7 是写保护位(CH),上电默认可能是 1,写时间前先写0x8E, 0x00解除写保护,写完再写回0x8E, 0x80。32.768kHz 晶振要选匹配负载电容的,起振电容一般 6pF 左右,焊错容量时间会跑偏。

4.5 按键一次触发多次

现象:按一下模式跳好几档。 原因:没消抖,或者主循环里按键扫描和显示刷新共用了长延时。 解决:key_scan里加 10ms 延时确认,并且用“按下沿”而不是“电平”触发逻辑,即记录上次状态,只在 1→0 时返回键值。

5. 进阶:把这份例程改成可移植的驱动模板

例程跑通之后,真正有价值的是把它拆成可复用的驱动层。我一般会把 LCD1602、DS18B20、DS1302 各自做成一对.c/.h,头文件里只暴露init、read、write、display这类接口,端口定义全部收进宏。这样换一块板子只改宏,不动逻辑。

具体做法是建一个board_config.h,把所有引脚映射集中管理:

// board_config.h #ifndef __BOARD_CONFIG_H__ #define __BOARD_CONFIG_H__ #define LCD_PORT P0 sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_EN = P2^2; sbit DQ = P3^7; // DS18B20 sbit CE = P3^4; // DS1302 sbit IO = P3^5; sbit SCLK = P3^6; sbit KEY1 = P3^0; sbit KEY2 = P3^1; sbit KEY3 = P3^2; #endif

驱动文件里只#include "board_config.h",不再出现具体端口号。验证方法很简单:把这份工程原样烧到另一块引脚布局不同的最小系统板上,只改board_config.h,如果显示、测温、走时全部正常,说明驱动层解耦干净了。如果还要动.c文件,说明哪里还藏着硬编码。

另一个值得做的进阶是给显示加一层缓冲。现在display_update是直接往 1602 写,如果主循环里同时有多个地方调显示,会互相打断。常见做法是在 RAM 里开一个unsigned char lcd_buf[2][16],所有逻辑只改缓冲,主循环末尾统一刷一次,刷的时候和缓冲比对,只写变化的字符。这样既减少 1602 写操作,也避免显示撕裂。

注意:加缓冲后要保证刷屏频率不低于 5Hz,否则设置模式下按键改数值会感觉延迟。我一般把刷屏放在定时器中断里,主循环只管改缓冲,这样节拍最稳。

从那以后我每次拿到一份 51 例程,都先做一件事:把端口定义全部抽到一个头文件里,再拿另一块板子验证一遍。能只改一个文件就跑通的,才敢往项目里放。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询