资讯动态

C51单片机LCD1602显示例程:集成DS18B20测温与DS1302时钟

发布时间:2026/10/17 7:26:10 来源:尧图企业网站定制
简介LCD1602与C51单片机结合的显示例程包含矩阵按键键值、DS18B20温度、DS1302时间、ADC0832电压四个实践项目面向单片机初学者及电子竞赛备赛者帮助理解字符液晶驱动和常用外设接口。压缩包共86个文件以C源码、Keil工程文件、Proteus仿真DSN、Hex烧录文件为主另含4段MP4仿真演示视频整体大小仅4.13MB便于下载与按目录分模块学习。已有1074人学习浏览内容覆盖按键扫描、单总线通信、实时时钟读写和模拟量采集等典型技能。通过对照源码和仿真录屏可以快速掌握LCD1602的RS、RW、E时序控制并能够将不同传感器数据格式化显示在屏幕上四个例程均配有独立工程目录便于代码复用与二次开发适合作为课程设计或入门实战的参考资料。1. LCD1602显示例程先把屏点亮再谈菜单和传感器上手C51开发板的人大多会经历这么一幕例程抄进去编译下载LCD1602却亮起两排小方块或者第一行有字第二行花屏。我当时做温控器光这块屏就折腾了一个晚上最后发现是VO对比度电位器和P0口上拉两个问题叠加。这份LCD1602C51单片机的显示例程解决的正是这种“看起来简单、接起来翻车”的尴尬。它不是一个跑马灯演示而是把LCD1602显示、按键输入、DS18B20测温、DS1302时钟芯片整合进同一个工程开机显示时间按一下键切到温度页再按一下切回时间页。适合刚把单片机基础跑通、想串多个外设做电子钟或温度计的新手也适合需要一份完整驱动代码做参照的开发者。先说结论显示例程的难点不在LCD指令而在把温度、时钟、按键三个外设放到一个while循环里还能不打架。2. 硬件接线把LCD1602、DS18B20、DS1302挂到C51的最小系统上2.1 LCD1602的16个引脚先分清数据线和控制线LCD1602兼容HD44780控制器引脚按功能分三组电源引脚VSS、VDD、VO控制引脚RS、RW、E数据引脚D0到D7。很多人上来就把D0到D7接P1口RS、RW、E再接P2口这样能用但后续串DS18B20和DS1302时会发现IO不够。我习惯把数据口放P0控制口放P2的高位这样P1和P3留给传感器和按键更从容。这里必须先说P0口的问题P0内部是开漏结构不带内部上拉。如果用P0接LCD数据线不接上拉电阻的话读回来的全是高电平LCD收到的指令字节统统变成0xFF表现就是清屏清不掉、光标乱跳。最省事的做法是在P0口接一个10K排阻或者直接外挂4.7K上拉电阻到VCC。功能LCD引脚C51连接数据位D0~D77~14脚P0.0~P0.7需外接上拉排阻RS寄存器选择4脚P2.6RW读写选择5脚P2.5也可直接接地E使能6脚P2.7VO对比度3脚10K电位器中间抽头背光灯15脚A、16脚KA接5VK串220Ω限流电阻接GND这里有一个可以省IO的写法如果例程不依赖忙标志只用延时等待LCD内部操作完成RW引脚可以直接接地RS和E两根线就够完成所有写操作。我自己的例程就是这种方案RW接地P2.5空出来给别的功能用。VO引脚接法值得单独说。它不是不接就能用的很多翻车现场都是因为VO悬空导致对比度过高或过低。正常做法是拿10K电位器接在VCC和GND之间中间抽头接VO上电后先旋到中间位置再根据显示效果微调。如果图省事把VO直接接地有些屏会看到QQ满屏方块还以为初始化写错了。2.2 DS18B20单总线只有一根线但上拉电阻千万别省DS18B20是单总线器件只有一个数据脚DQ。它的供电范围是3V到5.5V所以5V单片机上可以直接由来路供电。接法很简洁VDD接5VGND接地DQ接单片机IO口同时DQ必须接一颗4.7K上拉电阻到VCC。为什么不省这颗上拉电阻单总线靠高低电平的脉冲时序通信主机释放总线后DQ状态完全靠外部上拉拉到高电平。单片机IO口本身不是推挽输出拉高能力弱而DS18B20应答时要把总线拉低没有上拉的话电平识别就会模糊。最典型的现象是温度值忽高忽低或者一直读到85℃。我习惯把DQ放在P1.0因为P1口有内部上拉但依然会外接4.7K到VCC。双重保障避免那种“换成别家板子就失效”的玄学问题。2.3 DS1302三线接口与按键电路占用的IO只有三个DS1302实时时钟芯片是SPI风格的三线接口CE使能、SCLK串行时钟、I/O数据线。接线就是三根线加电源几乎不需要外围器件除了秒脉冲输出脚可以悬空。它还需要一颗32.768kHz晶振和两颗6pF负载电容晶振要和芯片尽量靠近走线短一点否则容易出现时钟不走或者走时偏快的血泪问题。按键部分最省IO的做法是独立按键一个按键占一个IO口。我用了两个独立按键分别接P3.2和P3.3。因为51的P3口第二功能分别对应外部中断0和外部中断1如果后续想把按键改成中断触发这两个位置最顺手。按键另一端接GND默认状态靠IO口内部上拉维持高电平按下时读到低电平。注意这种情况下按键直接接地IO口必须是在输入模式下不要往按键引脚输出高电平不然按下瞬间等于短路。整体硬件拓扑就是这样P0口带着LCD1602的8根数据线P2.6和P2.7控制RS和EP1.0接DS18B20P3.5、P3.6、P3.7接DS1302的CE、I/O、SCLKP3.2和P3.3接按键。这样一个系统已经把主要外设都串起来了IO余量还够再扩展其他东西。3. 核心代码拆解从初始化到温度、时钟、按键的完整链路3.1 LCD1602驱动先写底层时序再封装成字符串显示LCD1602的底层操作本质就是往控制器寄存器里写指令和写数据。指令寄存器接收清屏、光标移动、显示开关这类命令数据寄存器接收要显示的字符编码。RS引脚用来区分这两类操作RS0写指令RS1写数据。先看底层的写命令和写数据函数sbit lcd_rs P2^6; sbit lcd_rw P2^5; sbit lcd_en P2^7; void lcd_delay_us(unsigned int t) { while (t--); } void lcd_write_cmd(unsigned char cmd) { lcd_rs 0; // 写指令模式 lcd_rw 0; // 写操作本例中RW接地后此句可省 P0 cmd; // 指令字节送到数据口 lcd_en 1; // 使能拉高 lcd_delay_us(50); // 维持高电平 lcd_en 0; // 下降沿锁存数据 lcd_delay_us(500); // 给LCD内部处理留时间 } void lcd_write_data(unsigned char dat) { lcd_rs 1; // 写数据模式 lcd_rw 0; P0 dat; lcd_en 1; lcd_delay_us(50); lcd_en 0; lcd_delay_us(500); }这里有个关键点HD44780的E引脚是下降沿锁存数据所以E引脚每次都要从高拉到低才能在下降沿把P0上的数据锁进LCD内部。很多精简例程只写lcd_en 1; lcd_en 0;不加延时在12MHz晶振下也能工作但换成不同型号的屏或者晶振频率改变就会出现偶发乱码。我在E引脚高电平到低电平之间加了50微秒延时宁可慢一点也不赌时序。接着看初始化函数void lcd_init(void) { lcd_write_cmd(0x38); // 8位模式、2行显示、5x7点阵 lcd_write_cmd(0x06); // 写入字符后地址指针自动加1 lcd_write_cmd(0x0C); // 开显示、关光标、关闪烁 lcd_write_cmd(0x01); // 清屏 lcd_delay_us(2000); // 清屏需要较长时间 }0x38这条指令决定通信模式。如果只接D4到D7四根数据线那就要用0x28并且写字节时要分两次发送高低半字节。我的例程用完整8位模式P0口的8根线全接上代码直观排查问题也容易。0x06的作用是光标自动右移这样连续写字符串时字符不会原地覆盖。0x0C关闭光标闪烁适合显示温度和时间这种持续变化的内容不然光标一闪一闪很干扰读数。再封装一个定位函数和字符串输出函数void lcd_set_cursor(unsigned char row, unsigned char col) { if (row 0) lcd_write_cmd(0x80 col); // 第一行DDRAM地址从0x00开始 else lcd_write_cmd(0xC0 col); // 第二行DDRAM地址从0x40开始 } void lcd_show_string(unsigned char row, unsigned char col, unsigned char *str) { lcd_set_cursor(row, col); while (*str ! \0) { lcd_write_data(*str); } }0x80和0xC0这两个地址是LCD1602两行的首地址。第一行地址0x80对应DDRAM地址0x00第二行0xC0对应DDRAM地址0x40。col取值范围0到15超出就会写到下一行的越界位置或者不回显。封装好这两个函数后在main函数里显示一句话只需一行调用。3.2 按键扫描延时消抖加上电平确认按下才算数机械按键按下瞬间会产生抖动电平在0和1之间跳变好几次每次跳变大约持续几毫秒到十几毫秒。如果直接判断电平一次按键可能会被识别成多次动作。最简单的消抖就是延时后再次确认电平。sbit key_switch P3^2; unsigned char key_scan(void) { if (key_switch 0) // 检测到低电平 { lcd_delay_us(10000); // 延时约10ms跳过抖动区 if (key_switch 0) // 再次确认确实按下 { while (key_switch 0); // 等待松手 return 1; // 返回按键事件 } } return 0; }这个函数同步阻塞等待松手循环期间单片机不干别的活。在主循环里调用时按下期间会卡在while那里表面看没什么问题但如果同时要实时刷新温度或走时钟就会出现按键按下去的瞬间数码显示停顿。所以实际工程里我一般只保留10ms消抖不等待松手用松手后的电平变化在下一次主循环再处理。例程里保留等待松手的方式是为了让新手看清按键事件的完整流程。参数说明消抖延时不是越长越好10ms到20ms是机械按键的常见抖动范围。延时太短过滤不干净太长则按键响应变迟钝。如果按键接在P3口也可以改成外部中断加软件消抖但那样代码复杂度高不少对学习例程来说有点本末倒置。3.3 DS18B20温度读取单总线复位、写字节、读字节三件套DS18B20的通信协议全是时序活核心是三个函数复位、写一个字节、读一个字节。先看复位sbit ds18b20_dq P1^0; unsigned char ds18b20_reset(void) { unsigned char presence; ds18b20_dq 0; // 主机拉低总线 lcd_delay_us(500); // 至少480us复位脉冲 ds18b20_dq 1; // 释放总线 lcd_delay_us(60); // 等待DS18B20应答 presence ds18b20_dq; // 读到低电平表示有器件应答 lcd_delay_us(400); // 等待复位周期结束 return presence; }复位脉冲是主机先拉低480到960微秒再释放总线然后DS18B20会在15到60微秒内把总线拉低60到240微秒作为存在应答。presence为0表示有设备在线为1表示总线上没有DS18B20。我做调试时第一步就是看这个返回值如果为1后面全不用谈直接查接线和上拉电阻。继续看写字节void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { ds18b20_dq 0; // 每个位周期开始都拉低总线 if (dat 0x01) // 写1 { ds18b20_dq 1; // 拉高表示1 lcd_delay_us(60); // 保持高电平 ds18b20_dq 1; } else // 写0 { lcd_delay_us(60); // 保持低电平表示0 ds18b20_dq 1; } dat 1; } }单总线写时序的规则是每个时隙至少60微秒写0时整个时隙维持低电平写1时先拉低再快速拉高然后维持到时隙结束。移位操作从低位开始这是DS18B20的固定字节序。读字节函数unsigned char ds18b20_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { ds18b20_dq 0; // 发起读时隙 _nop_(); // 拉低后等1us左右再释放 ds18b20_dq 1; _nop_(); dat 1; if (ds18b20_dq) // 在15us内采样总线电平 dat | 0x80; lcd_delay_us(60); // 等待时隙结束 } return dat; }读时序的关键是从拉低总线到采样数据的窗口很短必须在释放总线后的15微秒内读取电平这个时间窗口内的总线状态代表1或0。超过窗口再读读到的是总线被DS18B20释放后的高电平全部变成1。实际测量温度时完整流程是发送跳过ROM指令0xCC然后发转换指令0x44等待750毫秒12位分辨率时再复位再发0xCC和读暂存器指令0xBE然后连续读两个字节得到温度低字节和高字节最后拼接成分数部分和整数部分。温度转换和读取必须分成两步做主循环里至少要有750毫秒的间隔否则读到的还是上一次的旧温度。3.4 DS1302时间读取三线时序与BCD码转换DS1302读写时分两条路径写是用SCLK上升沿把数据送入芯片读是在SCLK下降沿从芯片采出数据。先看写字节函数sbit ds1302_ce P3^5; sbit ds1302_io P3^6; sbit ds1302_sclk P3^7; void ds1302_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { ds1302_io dat 0x01; // 先放数据位 ds1302_sclk 0; // 拉低时钟 ds1302_sclk 1; // 上升沿锁存 dat 1; // 换下一位 } } unsigned char ds1302_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; ds1302_sclk 0; ds1302_sclk 1; // 下降沿输出数据 if (ds1302_io) // 采样数据线 dat | 0x80; } return dat; }这里我统一先拉高再拉低等效于每来一个时钟周期采一次数据线。DS1302的时序窗口比较宽裕延时只要几微秒即可。写入寄存器时先传命令字节再传数据字节读取时先传命令字节再读数据字节。命令字节的最低位置1表示读置0表示写。读写时间的封装函数unsigned char ds1302_read_time(unsigned char reg) { unsigned char dat; ds1302_ce 0; ds1302_sclk 0; ds1302_ce 1; // CE拉高启动通信 ds1302_write_byte(reg | 0x01); // 命令字节bit01读 dat ds1302_read_byte(); ds1302_ce 0; // CE拉低结束通信 return dat; } unsigned char bcd_to_dec(unsigned char bcd) { return (bcd 4) * 10 (bcd 0x0F); }DS1302内部时间是BCD码存储的比如秒寄存器里存的是0x35而不是十进制35。转换公式是高4位是十位低4位是个位。显示时间前必须经过bcd_to_dec转换否则屏幕上会出现十六进制的怪异数字。3.5 主循环把三个外设串起来按键切换时间页和温度页前面所有驱动都封装好后主函数实际上只是把各部分组装起来。逻辑是开机初始化LCD读取DS1302时间加温度数据然后进入主循环。按键用来切换显示页第一页显示时间第二页显示温度。void main(void) { unsigned char page 0; unsigned char key 0; unsigned char hour, min, sec, temp_int; lcd_init(); lcd_show_string(0, 0, LCD1602 Example); lcd_delay_us(3000); while (1) { key key_scan(); if (key 1) { page !page; // 按一次切换页面 lcd_write_cmd(0x01); // 清屏 } if (page 0) { hour bcd_to_dec(ds1302_read_time(0x84)); min bcd_to_dec(ds1302_read_time(0x82)); sec bcd_to_dec(ds1302_read_time(0x80)); lcd_show_string(0, 0, Time:); lcd_show_string(0, 5, : : ); lcd_write_cmd(0x80 5); // 定位到第一位数字 lcd_write_data(hour / 10 0); lcd_write_data(hour % 10 0); lcd_write_data(:); lcd_write_data(min / 10 0); lcd_write_data(min % 10 0); lcd_write_data(:); lcd_write_data(sec / 10 0); lcd_write_data(sec % 10 0); } else { // 发送温度转换命令并读取 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x44); lcd_delay_us(6000); // 简化处理实际建议750ms ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); temp_int ds18b20_read_byte(); ds18b20_read_byte(); // 忽略小数部分 lcd_show_string(0, 0, Temp:); lcd_write_cmd(0x80 5); lcd_write_data(temp_int / 10 0); lcd_write_data(temp_int % 10 0); lcd_write_data(0xDF); // 摄氏度符号 lcd_write_data(C); } } }主循环有几个坑需要注意。第一个是温度转换不能真的只延时600多微秒DS18B20在12位分辨率下转换时间需要750毫秒例程里这个延时是为了页面切换时能快速看到更新效果实际做产品时要么把延时拉长要么在等待期间去刷新其他内容。第二个是lcd_show_string里设置了光标位置后面用数字直接写显示时一定要记得先lcd_write_cmd(0x80 col)定位不然数字会接在字符串末尾继续往后写。4. 避坑指南不显示、乱码、85℃、按键失灵逐个排查4.1 背光不亮或字太淡先查VO电位器和背光限流电阻现象上电后LCD背光灯亮但字符区域一片空白或者字淡到几乎看不见。原因VO引脚对比度电压设置不对。VO电压决定了液晶显示的对比度电压太高或太低都会让字符无法正确显示。另一个原因是背光的A和K接反或者限流电阻阻值太大导致背光暗淡。解决先用螺丝刀调10K电位器从一头旋到另一头观察屏幕变化。正常位置一般在中点附近此时字符清晰、底色不糊。如果怎么调都没有字用万用表量VO电压应该在0到5V之间可调。要是固定在某一个值说明电位器接线有问题。背光那一端A接5VK经过220Ω电阻接GND电阻阻值越小背光越亮但也不要小于100Ω否则灯珠寿命受影响。4.2 满屏方块或乱码初始化方式与E引脚时序的配合问题现象上电后LCD显示两排黑色方块写入任何字符都是方块或者某一行正常另一行花屏。原因最常见的是初始化指令没生效0x38写入失败。原因是RS或E引脚接错P0口没加上拉电阻导致数据口电平不对或者初始化时两次指令间隔太短LCD内部还没处理完上一条指令就被打断。解决先确认RS接P2.6、E接P2.7和代码一致。然后用示波器或万用表量P0口是否有电平变化没有变化很大概率是上拉缺失或P0口被其他外设占用。初始化时清屏指令0x01执行需要1.64ms我习惯在0x01后面加2ms延时。如果还是花屏把8位模式改成4位模式试试有些LCD模块在8位模式下对时序更敏感。4.3 温度永远显示85℃单总线上拉电阻没接时序采样窗口太窄现象DS18B20读出来的温度永远是85℃不变化。原因85℃是DS18B20上电后的默认温度值。它表示芯片其实已经正常应答了但转换命令没有真正执行成功。通常是因为DQ引脚上拉电阻缺失导致单片机释放总线后电平不稳定转换指令数据传不到位。解决在DQ引脚和VCC之间加4.7K上拉电阻这是最关键的一步。其次检查复位脉冲宽度是否足够C51在12MHz下一次nop大约1微秒我代码里用延时函数模拟的500微秒要确实达到480以上。如果用的延时时长不准把延时值加大到800微秒不影响协议正确性但不要缩到400微秒以下。4.4 按键按下没反应或连按两次消抖不彻底和引脚悬空现象按一下按键屏幕页面连续切换两次或者偶尔按一次没反应。原因按键抖动导致按键扫描函数读到了多个下降沿引脚悬空会导致电平漂移在没有外部信号的情况下也可能触发误判断。解决消抖延时加到10ms以上在松手检测后再加一次短延时等电平静置稳定。对于P3口按键代码里该引脚设置为高阻输入后必须确认单片机内部上拉已经使能或者直接在按键引脚到VCC接10K上拉电阻。我通常两者都做外加上拉电阻比依赖内部上拉靠谱。4.5 时钟不走字或时间乱跳写保护位没关和晶振没起振现象调好的时间写入后断电再上电时间停在原来位置不动或者走几分钟跳一大截。原因DS1302有一个写保护寄存器地址0x8E最高位WP1时禁止写入任何寄存器。初始化时没把WP清零时间数据写不进去所以掉电后时间根本不会更新。走时跳变则是32.768kHz晶振焊接不良或负载电容不匹配。解决初始化流程里必须写操作ds1302_write_byte(0x8E); ds1302_write_byte(0x00);先把写保护关掉。晶振问题则是重焊晶振确保没有虚焊负载电容不要省6pF到12.5pF范围内根据晶振规格选。如果手头没有负载电容至少把晶振引脚尽量短一点不要绕长走线。5. 二次开发从显示例程改造成温控面板菜单这套例程跑通后最值得做的一件事是把它改造成一个带菜单的温控面板。改造思路不是推翻重来而是在现有驱动层上增加一层应用逻辑。第一步把页面切换从两个固定页面扩展成三页菜单时间页、温度页、设置页。在main里定义一个page变量和一个菜单数组每个菜单项对应一个显示函数。按键不再只做翻转而是根据当前page决定跳到下一页还是进入设置。设置页里再用按键调整温度阈值这一步也就几分钟的事。第二步利用LCD1602的自定义字符功能。HD44780支持8个自定义字符每个字符用8字节点阵数据描述。我做过一个温度计图标用CGRAM地址0x40到0x47写入点阵数据然后通过lcd_write_data(0x00)到0x07调用。核心代码先把点阵数组写进CGRAM再显示unsigned char code temp_icon[8] { 0x04, 0x0A, 0x0A, 0x04, 0x04, 0x0E, 0x1F, 0x0E }; void lcd_write_cgram(void) { unsigned char i; lcd_write_cmd(0x40); // CGRAM地址0x40 for (i 0; i 8; i) lcd_write_data(temp_icon[i]); lcd_write_cmd(0x80); // 切回DDRAM }第三步把主循环的查询式刷新改成定时刷新加事件驱动。用定时器0产生1ms中断维护一个计数器每500ms刷新时间显示每1秒刷新温度。按键不再阻塞等待松手而是把按键事件放进一个全局变量主循环只消费事件。这段改造能让刷新与按键互不干扰也是从例程代码走向工程化代码的关键一步。我从那次以后每次改这类多外设工程都强制自己先画一张外设引脚占用表再写驱动层最后才写应用逻辑。这个习惯避免了好几次接错线、改错引脚的低级返工。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑