☰
LCD1602+C51显示例程:整合DS18B20温度与DS1302时钟的完整驱动拆解
2026/10/17 11:10:25 网站建设 项目流程

简介:这是一套面向C51单片机初学者的LCD1602显示例程合集,围绕字符液晶与常用外围模块的联动,覆盖矩阵按键键值、DS18B20温度、DS1302时钟、ADC0832电压转换四种典型场景。资源共86个文件,核心包含Keil工程(C源码/uvproj/hex)、Proteus仿真电路(DSN)以及四段演示mp4,压缩包仅4.13MB,按矩阵按键、DS18B20、DS1302、ADC0832分目录存放,便于对照学习。已有1074人浏览学习。代码中涉及单总线通信、串行时序、按键扫描与模数转换等知识点,视频录屏配合实际工程,能帮助新手理解从硬件连接到数据显示的完整流程,也可作为课程设计或电子设计竞赛的参考模板。

1. 这个例程包解决的不只是“点亮LCD”:它把显示、按键、温度、时钟串成了一条完整链路

半夜调一块 LCD1602 出不来字,应该是很多做 C51 课设的人共同的记忆。插上开发板、烧进点灯程序、满怀期待地把 16 针排线怼上去,结果屏幕要么全黑要么只有第一行冒出一排方块。多数人卡在这里就放弃了,但真正的问题往往不是液晶坏了,而是初始化时序、数据口电平、E 脚的下降沿锁存这些细节没对齐。

这份 LCD1602+C51 单片机显示例程,我拆完之后的感觉是:它没有停留在“让屏幕亮起来”这一步。例程里同时带了按键切换、DS18B20 温度采集和 DS1302 时钟芯片读写,四个模块共用一套 51 的 GPIO 口,最后在 LCD 上动态显示时间和温度。对正在做课程设计、或者想在 51 开发板上体验“一板多用”的开发者来说,这份例程是把裸机外设驱动整合进一个状态机的完整参照——不是跑马灯 demo,而是一台能看夜晚温度的桌面信息屏。

2. 硬件接线与工程结构:四个模块怎么在 C51 上共存

拿到例程的第一步不是打开源码,而是先看它的文件和引脚规划。跨模块项目最怕的其实是引脚冲突:LCD 已经占了 11 个引脚,DS18B20 还要一根单总线,DS1302 又是三根线,加上三个按键,一个 40 脚的 AT89C52 都显得紧张。这份例程能把它们理顺,关键在“复用”与“错开”。

2.1 例程包文件结构与代码组织

解压后的工程目录不算复杂,但划分得很清楚,典型的模块化写法:

lcd1602_demo/ ├── main.c // 主循环、状态机、按键调度 ├── delay.c / delay.h // 软件延时,us 和 ms 两级 ├── lcd1602.c / lcd1602.h // LCD 驱动,命令与数据函数 ├── ds18b20.c / ds18b20.h // 单总线时序,温度读取 ├── ds1302.c / ds1302.h // 三线时序,时间读写 └── README.txt // 接线说明与引脚定义

每个外设一个 .c/.h 文件对,头文件里只暴露接口函数,不暴露全局变量,这是 51 裸机工程里比较容易忽视的好习惯。main.c 里不堆驱动代码,只做初始化、按键扫描和显示调度,这样后续把显示逻辑换成 OLED 或者加一个按键菜单,不需要动 DS18B20 那部分。

delay.c是这段例程的地基。LCD 和单总线时序对延时的依赖非常大,常见的写法是用_nop_()做微秒级延时,用循环嵌套做毫秒级延时。注意这里没有用定时器中断做延时——对这三块外设来说,阻塞式延时反而更可控,中断会打断时序的连续性,这是 51 外设驱动里一个很核心的原则。

2.2 LCD1602 并口接线:P0、P2 的选择与上拉电阻

看 README 里的引脚定义,数据口用的是 P0,控制口用 P2 的低三位。这个选择有讲究:P0 口内部没有上拉电阻,是开漏结构,接 LCD 数据线时必须在外部加上 4.7kΩ-10kΩ 的上拉排阻,否则高电平会飘,LCD 读出花字。P1、P2、P3 内部自带弱上拉,如果想把数据口改到 P1 或 P2,可以省掉排阻,但改到 P0 是大多数人开发板默认的布局。

// 引脚宏定义,集中在一个文件里方便改线 #define LCD_DATA P0 // 数据口 D0-D7 接 P0.0-P0.7 sbit LCD_RS = P2^0; // RS: 0=命令, 1=数据 sbit LCD_RW = P2^1; // RW: 一般直接接地或拉低 sbit LCD_E = P2^2; // E: 使能脚,下降沿锁存数据

RS、RW、E 三条控制线的分工要记牢:RS 决定当前传的是命令还是要显示的字符数据,RW 决定读还是写,E 的下降沿让 LCD 内部把总线上的数据锁存进去。RW 在只写不读的场景下可以直接接地,省一个引脚。但这份例程里保留了 RW 接 P2.1 的写法,是为了兼容“读忙标志”的扩展——虽然实际代码里用的是固定延时等待,没有去读 BF 位,但这种留余地的做法值得学。

2.3 DS18B20、DS1302 与按键的引脚分配

单总线、时钟三线、按键这三组外设,例程把它们分别放到了 P3 的不同引脚。DS18B20 的 DQ 接在 P3.4,DS1302 的 CE、SCLK、IO 接在 P3.5-P3.7,按键接在 P3.0-P3.2。这一排引脚最大的特点是:它们跟串口下载引脚 P3.0/P3.1 错开了,调试时不会互相干扰。

模块引脚说明
DS18B20DQ → P3.4单总线,需要 4.7kΩ 上拉
DS1302CE → P3.5 / SCLK → P3.6 / IO → P3.7三线 SPI 变体
按键S1 → P3.0 / S2 → P3.1 / S3 → P3.2接 GND,按下为低电平

DS18B20 的 DQ 脚必须加 4.7kΩ-10kΩ 上拉到 VCC。这是很多第一次用这款传感器的人最容易翻车的点:单总线在空闲时靠上拉电阻维持高电平,器件应答时靠拉低总线产生低电平窗口。没有上拉,复位脉冲之后总线浮空,读回来的存在应答就变成随机值。

2.4 电源与电平匹配:5V 与 3.3V 模块混接要小心

51 开发板供电多是 5V,而不少 LCD1602 模块和 DS1302 模块自带 3.3V 稳压或逻辑电平转换。混接时最典型的坑是:DS1302 的数据脚 IO 被 5V 电压拉高后,如果模块是 3.3V 电平逻辑,可能永久性损伤。可靠的做法是给 DS1302 的 CE、SCLK、IO 三条线都串一个 1kΩ 限流电阻,或者直接选择带电平转换的模块,例程 README 里也专门提示了这一点。

上电前的检查顺序我一般固定走一遍:先量 VCC 和 GND 是否短路,再查每个模块的背光跳线,最后用万用表确认 DQ 引脚上的高电平确实是接近 VCC 的,而不是一个悬空导致的 2V 漂浮电平。这一步能省掉后面整整一个晚上的排查时间。

3. 核心模块拆解:从按键按下的 0.5 秒到屏幕刷出的那一行字

这章是整份例程最值钱的部分。我之前拆过不少 51 显示例程,LCD 驱动基本都大同小异,真正的分水岭在于:单总线的读时序卡得准不准、DS1302 的写保护有没有关干净、以及按键切换和刷屏之间会不会互相拖累。

3.1 LCD1602 写命令与写数据:HD44780 时序的实现

先看最底层的写函数。HD44780 控制器的时序要求是:RS 和 RW 电平先稳定下来,然后 E 拉高,数据在总线上稳定一段时间后,E 降下去,这个下降沿就是数据被锁存的时刻。

// 写命令:向 LCD 写入控制命令字 void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; // RS=0 代表命令模式 LCD_RW = 0; // 写模式 LCD_DATA = cmd; // 命令字放到数据口 LCD_E = 1; // 使能拉高 delay_us(5); // 等待数据稳定 LCD_E = 0; // 下降沿锁存命令 delay_us(20); // 命令执行时间 } // 写数据:向 LCD 写入要显示的字符 ASCII void LCD_WriteData(unsigned char dat) { LCD_RS = 1; // RS=1 代表数据模式 LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; delay_us(5); LCD_E = 0; delay_us(20); }

这两个函数的区别只在 RS 那一位的设置上。HD44780 规定 RS=0 时写入被当作命令解析,RS=1 时数据直接进显存 DPRAM,所以 RS 的电平必须在 E 下降沿之前稳定下来。代码里先置 RS、再放数据、再拉 E,这个顺序就是时序规范里说的“建立时间”。

delay_us(5)和delay_us(20)两个参数实际上闲庭信步地满足了 HD44780 的需求:E 高电平最少要维持 450ns,命令执行最长要 1.64ms,这里的 20us 只覆盖短命令,长命令如清屏、光标归位实际需要 1.64ms,所以初始化序列里对这两条命令单独做了 5ms 延时。这是例程里一个容易忽略但非常关键的细节——如果你把它们统一缩短到 500us,屏幕就会表现为经常闪一下然后丢字。

初始化序列也是同样的套路,依次发送 0x38(8 位模式、两行、5x7 点阵)、0x0C(开显示、关光标)、0x01(清屏),每条命令之间做足延时:

// LCD 初始化:上电后必须等待内部自检完成 void LCD_Init(void) { delay_ms(15); // 上电等待 LCD_WriteCmd(0x38); // 8位模式、两行显示、5x7字体 delay_ms(5); LCD_WriteCmd(0x38); LCD_WriteCmd(0x0C); // 开显示 LCD_WriteCmd(0x01); // 清屏 delay_ms(5); }

重复发送两次 0x38 是老工程师留下的习惯。因为 51 上电后电源爬升速度一旦偏慢,LCD 内部状态机会进到一个未知初值,第一次命令可能被吞掉。有了这条“冗余训练”,容错性明显好了,实测在电压波动大的同学开发板上,屏幕出字的概率高了很多。

3.2 DS18B20 单总线时序:复位、存在应答与温度拼接

DS18B20 是整个例程里最敏感的模块,它的时序是以微秒为单位卡的。看它的复位函数,核心是“拉低 480us → 释放 → 采样存在应答”这组动作:

sbit DQ = P3^4; // 数据线定义 // 复位 DS18B20:返回 0 表示器件存在,1 表示未检测到 unsigned char DS18B20_Reset(void) { unsigned char presence; DQ = 0; // 主机拉低总线 delay_us(500); // 复位脉冲,要求 480us-960us DQ = 1; // 释放总线,进入接收状态 delay_us(60); // 等待器件应答窗口 presence = DQ; // 器件会拉低总线 60-240us 表示“我在” delay_us(400); // 等待应答结束 return presence; }

这段逻辑里最容易出问题的就是delay_us(500)这个时长。标准规定复位脉冲在 480us-960us 之间,太短器件不认,太长会把器件误判成读时序。另外释放总线后 60us 才开始采样,这个窗口是器件应答的最前端,错过了就永远读不回存在应答。我从例程里学到的技巧是:把delay_us(60)放到presence = DQ的前面,而不是先采样再延时,顺序反了读到的永远是高电平。

读一个字节的时序则是另一种套路,每个 bit 都要占 60us 到 120us:

// 读取一个字节,LSB 在前 unsigned char DS18B20_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; // 起始信号 delay_us(2); // 主机拉低 2us 左右 DQ = 1; // 释放总线 delay_us(8); // 在 15us 内采样 if (DQ) dat |= 0x80; // 高电平读 1,低电平读 0 delay_us(60); // 完成一个时隙 } return dat; }

读时序的关键窗口是:主机拉低总线后,从 15us 开始采样,到 45us 之前必须读完。代码里用delay_us(2)拉低,然后只等delay_us(8)就采样,总共 10us,落在安全区间内。如果把采样前的延时加长到 30us,实际上就读到了器件释放总线后的高电平,结果全变成 0xFF。这个坑我在自己项目里踩过一次,最后是靠示波器抓波形才看出是采样点太靠后了。

温度读取的完整流程是:跳过 ROM → 启动温度转换 → 等待 750ms → 跳过 ROM → 读两个字节。例程里没有死等 750ms,而是采用定时查询的方式,在DS18B20_ReadTemp里只发启动命令,转换结果在下一秒轮询时读回来,这样主循环不用傻等,LCD 刷新不会被卡住。

温度拼接的逻辑才是这章最“值钱”的 5 行代码:

// 拼接温度值:高字节左移8位后与低字节合并 temp_raw = (ds18b20_read_byte() << 8) | ds18b20_read_byte(); if (temp_raw & 0x8000) // 负温度判断 celsius = ((temp_raw & 0x7FFF) >> 4) * -1; else celsius = (temp_raw >> 4);

DS18B20 的 12 位分辨率表明,低 4 位是小数部分,整数部分是从第 4 位开始的 8 位。正温度直接右移 4 位就是整数;负温度存的是补码,不能直接右移,要先去掉符号位再取负,否则你会读到一串 120 多度的诡异数字。这里的>> 4是真正关键的一步。

3.3 DS1302 三线接口:寄存器映射与写保护开关

DS1302 的时序比 DS18B20 温和不少,它的三线接口是靠时钟线 SCLK 的上升沿和下降沿来区分读写方向的。每次写寄存器的第一步是先往命令寄存器写一个字节,告诉芯片“我要写哪个地址、接下来数据往哪放”。

// 向 DS1302 写一个字节,从最低位开始 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_IO = dat & 0x01; // 最低位先出 DS1302_SCLK = 0; DS1302_SCLK = 1; // 上升沿写数据 dat >>= 1; } }

DS1302 的命令字格式是0x80 | 寄存器地址 | 读写位,写地址时最低位要为 0,读时为 1。整个例程里最容易让我这种新手翻车的是写保护寄存器:DS1302 默认写保护是开启的,直接写时间寄存器根本没反应,必须先向 0x8E 这个地址写入 0x00 关掉写保护,改完时间之后再把 0x80 写回去打开写保护,防止意外改写。

时间数据在 DS1302 内部以 BCD 码存放,这是个隐蔽的坑。比如 7 点整存的是 0x07,到 7 点过 10 分存的是 0x10。如果你直接按十进制格式去写 10,芯片里存的是 0x0A,读取回来会被解析成“零十分”,时钟就彻底乱了。例程里专门写了十进制和 BCD 的互相转换:

// 十进制转 BCD:秒、分、时都按两位 BCD 处理 unsigned char DecToBcd(unsigned char val) { return ((val / 10) << 4) | (val % 10); } // BCD 转十进制:给显示层直接用 unsigned char BcdToDec(unsigned char val) { return (val >> 4) * 10 + (val & 0x0F); }

这两个函数写出来以后,DS1302 驱动的基本功就完成了。读时钟时只需要按秒、分、时、日、月、周的地址顺序把寄存器各摸一遍,经过 BcdToDec 转换后塞进显示缓冲区。它的寄存器地址表不需要背,驱动文件头部的注释里都列了,每次用直接查。

3.4 主状态机:按键扫描、模式切换与显示刷新策略

前面三个模块都是底层驱动,真正让例程像“产品”而不是“测试程序”的,是 main.c 里的状态机。三个按键 S1、S2、S3 分别负责“切换显示页”“加一调整”“减一调整”,按下时的扫描必须消抖,否则一次按键会被判定成十几次。

// 按键扫描:10ms 消抖,返回有效按键号 unsigned char KeyScan(void) { if (Key1 == 0) { delay_ms(10); // 消抖 if (Key1 == 0) { while (Key1 == 0); // 等待松手 return 1; } } return 0; }

消抖 10ms 这个数值不是拍脑袋定的。机械按键的抖动期通常持续 5ms 到 15ms,10ms 消抖能把绝大多数抖动滤掉,同时不会让快速连按失效。while (Key1 == 0)等待松手这一步很关键,它保证一次物理按下只产生一个有效事件——如果省掉,你会看到按一下键屏幕连跳三页的“飞页”现象。

主循环的显示调度策略是这份例程最成熟的地方,它没有粗暴地在每次按键后全屏刷新:

while (1) { key = KeyScan(); if (key == 1) { page++; if (page > 3) page = 0; LCD_WriteCmd(0x01); // 切换页时清屏 } if (page == 0) // 温度页 { ReadAndShowTemp(); } else if (page == 1) // 时间页 { ReadAndShowTime(); } delay_ms(200); }

每 200ms 跑一次主循环,DS18B20 的温度转换需要 750ms,所以温度页实际上每 200ms 查询一次,最多经过四次轮询才读到一个新值,但屏幕上的温度变化已经足够流畅。时钟页则每秒从 DS1302 读一次,避免频繁占用 I/O 口。最关键的是,只有切换页面时才调用清屏指令,平时刷新只使用LCD_WriteData覆盖变化字符,清屏指令非常耗时,动不动就是 1.64ms,高频调用会肉眼可见地看到屏幕闪烁。

4. 编译烧录与避坑指南:把例程从 Keil 工程变成实物上的显示

代码读得再细,不烧进板子里都是纸上谈兵。这一章我把从新建工程、编译出 HEX 文件到实物点亮的完整过程走一遍,再列出五条拆机过程中实测踩过的坑。

4.1 Keil 工程组织:文件添加、目标芯片设置与 HEX 生成

拿到例程要在 Keil C51 里跑起来,第一步是新建工程,选择目标芯片型号。不同开发板的 51 内核芯片配置略有差别,但基本都是 8051 内核,编译链接结果通用性很强,这里不需要过于纠结具体型号,关键是把优化等级设置好。

工程配置里两个地方必须检查:一是 Output 选项卡里勾选 “Create HEX File”,否则编译成功后只在目录里生成.obj文件,烧录器认不了;二是 C51 选项卡里的优化选择,建议选 Level 8 并且关掉 “Variables in Overlay” 选项,否则单总线函数里的局部变量可能被优化到寄存器里,运行到一半被中断打飞。

添加文件到工程时,典型的错误是只添加 main.c 而漏掉 delay.c、lcd1602.c 等其它文件。Keil 不会自动把同目录的 .c 文件拉进工程,每个源文件的编译都是独立任务。正确做法是在工程面板里挨个添加,再把头文件所在的目录路径填进 C/C++ 选项卡的 Include Paths 里。编译后看到的 0 Error(s) 并不是终点,真正要等到 HEX 文件出现在输出目录里才算成功。

4.2 烧录与上电的排错流程

烧录的第一步是确认下载器驱动装好了,设备管理器里能看到串口。接下来选择 HEX 文件、设置波特率、冷启动下载——这对于那些需要冷启动的下载电路来说,是唯一可靠的烧录方式:先点软件里的“下载”按钮,再把开发板断电重新上电。

烧录完成后上电,最稳的验证顺序不是直接看 LCD,而是先打一个 LED 闪烁的最小程序,确认最小系统正常。然后逐步挂载外设:先只接 LCD 和电位器,调出两行蓝色方块;再接 DS18B20;最后接 DS1302。一次只接一个模块,哪一步挂了立刻就能定位到是接线还是代码问题,这条顺序能让你绕开整个“全部接上后集体沉默”的排查泥潭。

4.3 避坑记录:五条拆机过程实测踩过的坑

现象 1:LCD 第一行全是一排方块,第二行黑块原因:初始化时序里的命令被吞了,大概率是延时太短,或者对比度电位器根本没有调到位。 解决:拆开例程的 LCD_Init,核对每条命令后面的延时是否充足,特别是清屏后的 5ms;同时用螺丝刀微调背光板背后的电位器,调到第一行出现清晰的两个小光标块。

现象 2:DS18B20 读出来永远是 85℃原因:85℃ 是 DS18B20 上电复位后的默认温度寄存器值,意味着从未成功触发一次温度转换。 解决:重点检查复位脉冲时长是否落在 480us-960us,读取字节时每位的采样窗口是否过早或过晚。最常见到的情况是上拉电阻没有焊,DQ 引脚悬空,例程的延时参数本身没有问题,大概率被连接线背了锅。

现象 3:按键按下去,屏一口气跳两三个页面原因:没有等待松手,或者没有消抖,导致一次按下触发多次有效扫描。 解决:确认消抖延时至少 10ms,并且按键释放后才返回有效键值。我在这份例程的 main.c 里特意找到while (Key1 == 0);这一行,在它前面注释里标了“等待松手”,这行一个字都不能删。

现象 4:DS1302 时间写上去了,但断电重新上电后倒退回初始值原因:DS1302 的备用电池没有接,掉电后时钟芯片连振荡器都停转,寄存器内容全部复位。 解决:查看 5V 主电源之外是否给 DS1302 的 VCC2 引脚接了 3V 纽扣电池;接好备用电池后代码里还要检查写保护寄存器是否做了软件关断开合。

现象 5:烧录时软件永远提示“连接失败,请给 MCU 上电”原因:开发板的下载引脚 P3.0/P3.1 被外部器件占用,比如接在旁边的 DS18B20 或按键电路拉低了串口电平。 解决:把占用 P3.0/P3.1 的跳线帽拔掉再烧录。如果你发现了例程里把 DS18B20 放在 P3.4、按键放在 P3.0-P3.2 但跳过 P3.1,也正是它留了一手给下载电路。

5. 二次开发指引:把它改造成一台能显示三路数据的环境信息屏

把这份例程吃透之后,最值得做的事不是去背诵驱动函数,而是自己动手改造成套逻辑。我的习惯是拿到任何一块没玩过的屏幕或传感器,先跑通裸例程,再往里加自己的业务逻辑。这次也一样,我只打算把它的架构往两个方向延伸:扩展传感数据、优化显示调度。

5.1 第一步:加一个 DHT11 湿度传感器

DHT11 也是单总线协议,但时序跟 DS18B20 完全不同,可以完全不占用新的引脚,直接复用 P3.4 旁边的 P3.3。在驱动文件里仿照 ds18b20.c 写一套时钟启动脉冲、读 40 个 bit 的函数,然后把湿度值并存进显示缓冲区。注意 DHT11 每个 bit 的时隙是 26us-28us 宽,主频 12MHz 下用 nop 延时精度刚好,所有人都可以照抄。

// 读取 DHT11 湿度值并拼进显示缓冲区 void ReadAndShowHumidity(void) { unsigned char humi = DHT11_ReadByte(); LCD_WriteData('H'); LCD_WriteData(':'); LCD_WriteData(humi / 10 + '0'); LCD_WriteData(humi % 10 + '0'); LCD_WriteData('%'); }

写到这里我需要提醒一点:LCD 写数据函数里每个数字字符都要先转换成 ASCII 码再加0,而不是直接把整数往LCD_WriteData里塞。如果是负数或超过两位的数字,还要额外处理符号位和取整逻辑——这是新手改显示代码时最容易出现的黑匣子问题。

5.2 第二步:把按键翻页改成自动轮播

现在三个键控制四个页面,是手动切换。改造成自动轮播的思路很清晰:在主循环里加一个计数器page_count,每次循环page_count++,当它累计到 5000 时自动切换page = (page + 1) % 4,再清屏刷新,按键只保留“手动翻页”优先级。关键是让自动轮播和按键触发走同一条切换逻辑,不要写两份重复代码,这样任何一个页面切换 bug 都能从一处修复。

5.3 第三步:验证改造是否成功的三个标准

改造完了不能只看屏亮不亮,我给自己定三个验证标准:第一,温度页从 25℃ 加热到 35℃ 的过程中,数字只需在 1 秒内完成一次跳变,同时不能出现 25 和 76 这种乱码;第二,拔掉 DS1302 的备用电池,等待 10 秒后重插,上电时间应正确回到初值而非随机值;第三,连续按 200 次翻页键,页面不能卡死、不能漏帧。

这三个标准过了,这个信息屏才能算稳定可交付。做嵌入式开发这些年,我也不是没在单总线和时钟芯片上翻过车,自从第一次半夜排查 LCD 花屏到天亮之后,每次拿到新的显示例程,我都会强制自己走一遍“先量电平、再看时序、最后怀疑代码”的排查顺序。希望这篇拆解能帮你在点亮这块屏幕的时候,少走几个我走过的弯路,也少看几个凌晨两点的满天星。希望帮到你。

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

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

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

立即咨询