资讯动态

LCD1602+C51驱动例程:从显示时序到传感器联动开发

发布时间:2026/10/17 7:26:10 来源:尧图企业网站定制
简介一套针对LCD1602与C51单片机的综合显示例程面向电子设计初学者与嵌入式入门者解决字符液晶显示与常见外设的编程交互问题。例程共86个文件、压缩包4.13MB主体为C语言源码、Keil工程文件与Proteus仿真设计并附带操作演示视频可直接用于学习与二次开发。内容覆盖矩阵按键键值显示、DS18B20温度读取、DS1302实时时钟显示和ADC0832电压转换四种典型应用每个功能都配有对应源码与仿真电路便于对照理解C51的驱动时序与数据处理流程。已有1074人学习下载适合想快速掌握LCD1602驱动、单总线通信、串行时钟及模数转换的读者通过源码、仿真和视频三方面配合能系统性提升单片机外设调试能力。1. LCD1602 C51一个显示例程包能带你把接口、时序、传感器全跑通做单片机开发的人几乎都绕不开 LCD1602 这块字符液晶。它便宜、够用、引脚直插也是课程设计和实际产品里最常见的显示方案。这个例程包不是一个光杆的“Hello World”它把 LCD1602 的底层时序、按键扫描、DS18B20 温度读取、DS1302 时钟显示串成了一条完整的代码链路——拿到手烧进去就能看到字符翻动改一改就能移植到自己的项目里。适合正在做课程设计的学生也适合刚接触 C51 想快速点亮液晶的开发者。看完这篇你不仅能跑通例程还能自己修改引脚、调整显示内容甚至把四线驱动时序移植到 STC 或 AT89S52 上。2. 硬件连接与显示原理先把手上的液晶摸清楚再谈写代码2.1 两种驱动模式8 线快但费引脚4 线省引脚但时序绕LCD1602 的数据总线支持 8 位DB0~DB7和 4 位DB4~DB7两种模式。8 线模式一次传一个字节速度块但占用 8 个 I/O对引脚紧张的 C51 系统不太友好。4 线模式每次只传高 4 位分两次送完一个字节速度减半但引脚占用少一半这是多数例程包默认采用的方式。选择 4 线模式还有一层原因C51 常用 P0、P2 口做外部总线P1、P3 口留作按键和中断如果让 LCD 占掉 8 个引脚留给传感器和交互的余量就太少了。例程包里如果看到LCD_DataPort P1这样的宏定义而代码里只操作了 P1 的高四位那就是标准 4 线驱动。移植到其他板子时只需要改宏定义里的端口和引脚编号不用动时序函数。2.2 最小连接图与背光、对比度这两个“隐藏开关”LCD1602 一共 16 个引脚但真正必须接的只有 6 个RS、RW、E、DB4~DB7。电源方面 VSS 接地、VDD 接 5V背光的 LED 和 LED- 是两个容易被忽略的引脚——它们不能直接接到 5V 和 GND 上否则背光电流可能超出额定值导致液晶发烫甚至烧坏背光板。常见做法是串一个 10~20Ω 的限流电阻或者直接借用板子上的背光控制三极管。对比度引脚 V0第 3 脚是另一个玄学点。它需要一个 0~5V 之间的电压通常用 10K 电位器分压调到输出清晰但不出现“鬼影”的位置。有些例程包直接写“V0 悬空”或“V0 接 GND”这在某些批次液晶上会导致显示极淡或全黑。我一般会在 V0 和地之间并联一个 103 电容滤掉电源纹波省得在调对比度时显示内容跟着电源抖动。2.3 读忙标志还是死等延时时序选择的第一个岔路口写数据前单片机需要确认 LCD 内部是否还在处理上一条指令。官方时序表里有一个 BF 标志bit 7读取它就能知道液晶是否忙。读忙标志需要把 RW 置高、RS 置低再读 DB7逻辑上更“正规”但代码要额外处理引脚方向切换而且 C51 的 I/O 是准双向口读之前要先把端口写 1。例程包更常见的做法是“死等延时”每次写完指令后插入一段 1~5ms 的延时靠时间换确定性。这个方法在系统主频 12MHz 下非常稳定缺点是不够通用——如果换了 24MHz 晶振延时参数就得重新调。我的建议是例程能跑就不用动如果做产品优先改读忙标志哪怕多几行代码换来的是不同晶振下的稳定行为。3. 驱动代码落地初始化、写指令、写字符的三层封装3.1 引脚宏定义与延时函数——先定好规则再写逻辑拿到例程包先别急着编译打开头文件看引脚定义。这个例程的引脚绑定具有典型性RS 接 P2.0、RW 接 P2.1、E 接 P2.2数据口接 P1 的高四位。引脚宏定义用sbit关键字声明改移植时只需要动这几行。#define LCD_DataPort P1 // 数据口只用高四位 sbit LCD_RS P2^0; // 寄存器选择0命令1数据 sbit LCD_RW P2^1; // 读写选择0写1读 sbit LCD_E P2^2; // 使能信号高脉冲触发延时函数是 LCD 驱动的基石。12MHz 晶振下_nop_()约等于 1 微秒写命令需要至少 1ms 间隔。例程里通常封装了LCD_Delay1ms()和LCD_DelayNus()两个函数前者用于初始化定时后者用于 E 脚脉冲宽度控制。如果换用 STC 单片机且开了 IRC 时钟延时参数要按实际主频重算否则字符偶尔会丢。3.2 写命令与写数据RS、RW、E 三条腿的配合写命令和写数据的时序几乎一样唯一区别是 RS 电平。写命令时 RS0写数据时 RS1。RW 在写操作时固定为 0。E 脚是关键数据要在 E 下降沿之前稳定下来所以代码里先给数据、再拉高 E、延时、最后拉低 E。void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; // 命令模式 LCD_RW 0; // 写操作 LCD_DataPort (LCD_DataPort 0x0F) | (cmd 0xF0); // 送高四位 LCD_E 1; LCD_DelayNus(1); LCD_E 0; // 高电平脉冲结束锁存高四位 LCD_DelayNus(1); LCD_DataPort (LCD_DataPort 0x0F) | ((cmd 4) 0xF0); // 送低四位 LCD_E 1; LCD_DelayNus(1); LCD_E 0; LCD_Delay1ms(); // 指令执行时间 }这个函数做了四件事设置 RS/RW 电平、分两次送出 8 位数据、每个半字节产生一个 E 脉冲、结束后等待指令执行完成。参数cmd是要发送的指令码比如 0x38、0x0C。LCD_DataPort (LCD_DataPort 0x0F) | (cmd 0xF0)的意思是只改高四位不影响低四位原有的引脚状态——这在使用 P1 口同时接其他器件时特别重要。写数据函数结构完全一样只把LCD_RS 0改成LCD_RS 1。有些例程会额外加一个LCD_WriteData()的包装底层再调LCD_WriteByte()这种结构对后续封装字符串函数更友好。3.3 初始化序列0x38、0x0C、0x01 背后的含义LCD1602 上电后必须先初始化这个序列是芯片手册规定的标准流程不能乱序也不能省。例程包的初始化函数长这样void LCD_Init() { LCD_Delay1ms(15); // 上电等待让液晶内部复位完成 LCD_WriteCmd(0x38); // 8位数据模式2行显示5x8点阵 LCD_WriteCmd(0x0C); // 显示开光标关闪烁关 LCD_WriteCmd(0x01); // 清屏光标归位 LCD_WriteCmd(0x06); // 写入后地址自动加1光标右移 }0x38 是 8 位模式命令4 线模式初始化时有些例程会先发 0x28但在上电时直接发 0x38 再切 4 线流程兼容性更好。0x0C 控制显示开关——很多新手把光标和闪烁位也一起开了结果屏幕上多个下划线在跳。0x01 清屏需要时间最长约 1.6ms所以这条指令后的延时要比其他指令长一些。0x06 决定写入字符后地址是加 1 还是减 1默认加 1 就够日常用。注意 0x38 与 0x28 的差别前者是完整 8 线模式后者是 4 线模式。初始化流程必须与硬件连接一致如果硬件接的是 4 线但初始化发 0x38数据线会错乱。例程包如果只提供 8 线初始化你直接改成 4 线连接时屏幕上大概率出现双影字符。3.4 显示字符串DDRAM 地址与 0x80 的加法LCD1602 内部显示缓冲区DDRAM有两行每行 80 字节但屏幕只显示前 16 个。第二行的起始地址是 0x40而不是 0x10——这是最容易踩的坑。写地址指令要求最高位为 1所以定位第一行第 1 列要发0x80 | 0x00定位第二行第 1 列要发0x80 | 0x40。void LCD_ShowString(unsigned char row, unsigned char col, unsigned char *str) { unsigned char addr; if (row 0) { addr 0x00 col; // 第一行0x00 ~ 0x0F } else { addr 0x40 col; // 第二行0x40 ~ 0x4F } LCD_WriteCmd(0x80 | addr); // 设置DDRAM地址最高位置1 while (*str ! \0) { LCD_WriteData(*str); str; } }这个函数先算出目标地址再通过0x80 | addr构造写地址指令。参数row取值 0 或 1col取值 0~15。0x80是地址指令的标志位与地址做按位或运算后液晶才能识别这是“定位”而不是“显示字符”。写到最后一行时光标会自动换到下一行开头——这个行为由 0x06 指令里的地址加 1 模式决定。4. 把例程扩展成完整项目按键、DS18B20、DS1302 的联动显示4.1 按键扫描与显示切换状态机思维代替散装 if例程包里如果只提供 LCD 驱动那它只是个“显示工具”。配上按键后才算一个完整交互系统。常见做法是用三个按键一个切换显示界面一个加数值一个减数值。按键扫描要处理机械抖动一般用 10ms 延时消抖或者检查电平连续稳定。unsigned char key_scan() { if (KEY_UP 0) { // 检测到按下低电平有效 LCD_Delay1ms(10); // 跳过抖动区域 if (KEY_UP 0) { // 再次确认仍是低电平 while (KEY_UP 0); // 等待释放防止长按连续触发 return 1; // 返回按键编号 } } return 0; // 无按键 }这段代码顺序执行先快速检测第一次低电平延时 10ms 后再次检测若仍为低电平则确认按键有效然后死等释放。返回值 1 表示 UP 键按下。实际项目里如果按键接在 P3 口且内部有上拉电阻可以省略外部上拉如果是 P1 口需要检查外部上拉是否完整否则按键按下时电平不干净。显示切换的核心是状态变量current_page为 0 时显示温度为 1 时显示时间为 2 时显示设置界面。每次切换界面必须调用LCD_WriteCmd(0x01)清屏否则上一屏的残留字符会和新内容叠加看起来像乱码。例程包如果给了这种结构你往里面填业务逻辑就很快。状态机的好处是每个界面都有独立的刷新函数代码不会全部堆在 main 里。比如show_temperature()只负责读取 DS18B20 并格式化显示show_time()只负责读 DS1302。main 函数里只做按键判断和刷新调度这样扩展新界面非常容易——加一个 else if 分支就行。4.2 DS18B20 温度读取单总线时序与温度换算DS18B20 是单总线器件只有一根数据线所有通信靠时序区分。初始化的过程是主机拉低 480us 以上然后释放总线等待 DS18B20 拉低 60~240us 作为存在脉冲。这个时序对延时精度要求高主频不对会导致初始化失败典型表现是温度永远显示 85℃——这是 DS18B20 上电后的默认值说明代码根本没读到传感器。bit DS18B20_Init() { bit presence; DQ 1; LCD_DelayNus(10); DQ 0; // 主机拉低总线 LCD_DelayNus(500); // 最小480us DQ 1; // 释放总线 LCD_DelayNus(70); // 等待DS18B20响应 presence DQ; // 0存在1不存在 LCD_DelayNus(500); // 等待时序结束 return presence; // 返回0表示检测到设备 }初始化成功后写 ROM 指令 0xCC跳过 ROM 匹配单设备时用、功能指令 0x44启动温度转换然后等待转换完成。读温度时发送 0xCC 和 0xBE连续读两个字节得到温度低字节和高字节。温度为负数时两个字节是补码形式需要做符号扩展后再除以 16 得到实际温度值。例程包里的温度换算代码通常这样写把读到的低字节和高字节拼成一个 16 位整数右移 4 位得到整数部分小数值用0.0625乘以剩下的 4 位。如果只显示整数温度可以简化成temp 4但负数时 C51 的右移是算术右移还是逻辑右移取决于编译器这个细节要小心否则 -10℃ 可能显示成 240℃。4.3 DS1302 时间显示BCD 码的坑与读时序DS1302 是 SPI 接口的时钟芯片寄存器地址用命令字节的第 7 位指定方向0 写、1 读。时间数据全部以 BCD 码存储——比如 15 点存成 0x15而不是 15。如果直接把读到的字节拼成十进制显示会出现“09 点 90 分”这种奇怪的结果这就是 BCD 码没有转换。void DS1302_WriteTime(unsigned char addr, unsigned char dat) { unsigned char i; DS1302_CE 1; // 拉高片选开始通信 DS1302_IO 0; DS1302_SCLK 0; for (i 0; i 8; i) { // 发送命令字节 DS1302_IO addr 0x01; addr 1; DS1302_SCLK 1; // 上升沿写入 DS1302_SCLK 0; } for (i 0; i 8; i) { // 发送数据字节 DS1302_IO dat 0x01; dat 1; DS1302_SCLK 1; DS1302_SCLK 0; } DS1302_CE 0; // 拉低片选结束通信 }这个函数实现的是“先发命令字节再发数据字节”的标准时序。命令字节每一位是低位先出数据字节同样低位先出。我的习惯是在主函数里写一个小工具函数unsigned char BCD_to_DEC(unsigned char bcd) { return (bcd 4) * 10 (bcd 0x0F); }专门转换时间寄存器读出来的值。写入时反过来DEC_to_BCD把十进制数拆成高 4 位和低 4 位。DS1302 有一个防写入保护寄存器0x8E默认是使能状态不关掉它写任何时间都会失败。例程包里一般在初始化时执行一次DS1302_WriteByte(0x8E, 0x00)这个细节能省去排查“为什么调时间没反应”的大量时间。读时间时注意连续读取时要保持 CE 为高不能中途拉低否则读回来的数据会错位。4.4 整合示例主页轮播与按键切换的逻辑框架把上面几块拼起来就形成一个完整的显示系统。main 函数的大致流程初始化 LCD、DS18B20、DS1302设置初始时间然后进入死循环。循环里先做按键检测再根据当前状态刷新屏幕。void main() { LCD_Init(); DS18B20_Init(); DS1302_Init(); DS1302_SetTime(0x24, 0x08, 0x15, 0x01, 0x10, 0x30, 0x00); // 年/周/月/日/时/分/秒BCD码格式 while (1) { unsigned char key key_scan(); if (key 1) { current_page (current_page 1) % 3; LCD_WriteCmd(0x01); // 切页前先清屏 } if (current_page 0) { show_temperature(); } else if (current_page 1) { show_time(); } else { show_setting_page(); } } }这个框架的逻辑按一次键换一页三页循环。show_temperature()内部每秒刷新一次温度值——不停地调 DS18B20 读取会占用大量 CPU所以加了一个简单的秒计时判断当前读到的秒数与上次不同才重新读温度。这个优化对 DS18B20 特别重要因为它的转换时间最长需要 750ms频繁读取会让 CPU 卡死在等待循环里。设置页的加减功能就交给“散装 if”逻辑在设置状态下UP 键切换要调的分量小时/分钟/秒DOWN 键修改数值。例程包如果提供了这个框架你做起课程设计或毕设只需要改显示内容不需要动任何时序代码。5. 避坑手册LCD1602 显示例程里最常见的五个翻车现场5.1 第一行正常第二行全黑或显示乱码现象第一行字符显示完好第二行要么全黑块要么出现不认识的符号清屏命令也救不回来。原因定位第二行时地址算错了。很多人写LCD_WriteCmd(0x80 | 0x10)认为第二行从 0x10 开始但 DDRAM 第二行的物理起始地址是 0x40。0x10 对应的是第一行的第 16 列恰好超出屏幕可视范围数据写进了看不见的区域第二行自然就缺内容。解决把第二行定位改成0x80 | 0x40。如果你写了一个通用的LCD_SetCursor(row, col)函数检查 row1 时地址偏移量是不是 0x40。5.2 上电花屏、字符颜色深浅不一、过一会儿变淡现象刚上电时满屏方块或者花屏过几秒逐渐稳定但对比度不均匀屏幕上半部清晰、下半部发淡。原因对比度引脚 V0 电压不稳定。V0 悬空时电压浮空受环境干扰波动直接接地时对比度太高显示黑色块。背光电流过大也会导致屏内温度升高液晶响应特性漂移表现出“越用越淡”。解决V0 脚接 10K 电位器中心抽头电位器两端分别接 5V 和 GND先调到肉眼能看清为止。同时在 V0 与 GND 之间并联 104100nF电容吸收电源纹波。如果是 3.3V 系统V0 压差和 5V 不同对比度可分压电阻阻值要做调整不能直接照搬 5V 电路。5.3 代码逻辑正确但显示时快时慢、偶尔丢字符现象同一个循环里刷新字符串有时字符不全多刷新几次又恢复正常。Debug 时单步执行完全正常全速运行就出问题。原因延时函数不精确。C51 编译器的优化等级会影响空循环的编译结果——低优化时循环体保留高优化时可能被删掉一部分导致实际延时远小于设计值。E 脚高电平脉冲如果持续时间不够数据锁存失败丢字符就必然发生。解决把延时函数声明为void LCD_DelayNus(unsigned int n)内部用_nop_()组合实现微秒级延时而不是空循环。在 Keil 里把 LCD 相关文件的优化等级设置为 Level 0 或指定文件不优化保证时序稳定。测试方法是固定发送一个“A”如果能稳定显示 100 次不丢字符说明时序裕量足够。5.4 温度一直显示 85℃ 或显示负值乱码现象DS18B20 读取模块单独测试正常接上 LCD 后显示 85℃ 不更新或者显示 0.0℃ / 负数乱跳。原因85℃ 是 DS18B20 上电复位后的默认温度寄存器值如果没有真正启动温度转换就直接读结果永远是 85℃。常见原因是初始化时序中 480us 延时过短DS18B20 没来得及响应或者单总线引脚外部没有上拉电阻总线电平不够高。解决检查初始化函数里的延时长度是否严格满足数据手册要求复位 480us等待响应 60~240us。确保 DQ 引脚外部接一个 4.7K 上拉电阻到 VCC——C51 的 P1 口内部虽有弱上拉但驱动单总线的负载能力有限外部上拉几乎是必须的。如果显示负数乱码重点检查 16 位温度值的补码转换逻辑正负数的右移方式在不同编译器下可能不一致。5.5 时钟不走、掉电后时间归零现象DS1302 显示时间能调能走但断电后重新上电时间恢复初始值或者板子运行几分钟后时间忽然乱掉。原因两个坑叠加。一是后备电池接错DS1302 的 VCC2 引脚接主电源、VCC1 接电池如果两个电压源接反或缺失掉电后寄存器数据清零。二是写保护没有关闭时任何写入操作都被拒绝表现为“能读不能写”。解决确认 VCC1 接 3V 纽扣电池正极、VCC2 接系统 5V。初始化函数里先写 0x8E 为 0x00 关闭写保护再写时间寄存器。时间校准后重新打开写保护写 0x80防止意外修改。如果时钟走得不准——一天差几分钟——把 DS1302 的 32.768K 晶振负载电容调到 6pF 附近有些模块预留了焊盘位置晶振对地电容过大会导致频率偏慢。6. 把驱动改造成可复用模块接口封装与一个顺手的调试技巧例程包里的驱动代码大多是“平铺式”写法所有函数放一个文件里改引脚时容易牵一发动全身。第一次移植完我习惯把 LCD 驱动拆成 lcd.h 和 lcd.c 两个文件。lcd.h 里只放宏定义和函数声明lcd.c 里放底层时序函数和上层显示函数。这样每个新项目只需要复制这两个文件改完头文件里的引脚宏定义就能用。接口设计遵循一个原则上层函数不要直接操作引脚。比如LCD_ShowString(0, 0, Hello)内部自己算地址调用者不关心 DDRAM 地址怎么加也不用知道 0x80 标志位。这个习惯帮我省了大量调错时间——有一次换了一块不同品牌的 1602底层命令完全兼容但 DDRAM 的行起始地址不一致我只改了 lcd.c 里的LINE2_ADDR一个宏就搞定上层代码一行没动。一个很顺手的调试技巧把单片机串口重定向一个函数让 printf 直接打到 LCD 上。实现方式是写一个char putchar_lcd(char c)里面判断如果是\n就调用清除当前行并跳到下一行否则正常写字符。然后设置 Keil 里的PUTCHAR_PROTOTYPE重定向。比如调试 DS18B20 时直接在代码里写printf(Temp: %d.%d C, temp_int, temp_dec);就能在液晶上看到格式化后的温度不用手动拼数组。这个技巧特别适合看传感器数据变化趋势比用 Keil 的 Watch 窗口直观得多。晶振频率不同时示波器显示时序会有细微差异但请记住这个红线E 脚高电平常数至少在 450ns 以上初始化序列的 0x38 与 0x28 必须和硬件连接匹配0x80 地址标志位什么时候都不能丢。从那以后我每次拿到一块新液晶或新单片机都强制走一遍同样的流程看连接方式、确认 4/8 线模式、跑初始化序列、点亮背光、调对比度五步走完才写业务代码再也没有在显示问题上熬过夜。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑