ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

嵌入式实战:基于51单片机的12864液晶时钟温度计设计与避坑指南

嵌入式实战:基于51单片机的12864液晶时钟温度计设计与避坑指南 简介这是一份面向嵌入式初学者与电子设计爱好者的12864液晶显示多功能电子时钟项目资源聚焦于时间、温度双参数实时显示及重要节日提醒功能适用于单片机课程设计、毕业设计或DIY实践场景。资源包共20个文件含C语言源码.c、头文件.h、Keil工程配置.uv2、.opt、.plg、编译中间文件.obj、.lst、.a51及可执行固件.hex完整覆盖从代码编写、编译调试到烧录运行的全流程62KB体积精炼便于快速导入学习。已有124人下载学习资源结构清晰包含DS1302实时时钟、DS18B20温度传感器驱动及LCD12864图形显示模块的独立实现文件配套注释充分特别适合理解嵌入式外设驱动整合、低分辨率UI布局与农历/公历节日逻辑处理等核心技能。1. 项目缘起一个经典而实用的嵌入式练手项目最近在整理旧硬盘时翻出了一个尘封已久的压缩包名字就叫“LCD.rar”。解压一看里面是几年前用51单片机驱动一块12864液晶屏同时显示实时时钟和环境温度的一个小项目。代码虽然有些稚嫩但功能完整让我想起了当年刚接触嵌入式时那种从点亮一个LED到让屏幕显示出第一个字符的兴奋感。这个项目可以说是嵌入式入门的“毕业设计”之一它综合了GPIO控制、液晶驱动、实时时钟芯片通信、温度传感器读取等多个核心技能点麻雀虽小五脏俱全。今天我就以这个“LCD.rar”项目为蓝本和大家一起复盘一下如何用一块常见的12864液晶屏结合DS1302时钟芯片和一款温度传感器比如DS18B20打造一个功能完备的电子时钟与温度显示器。无论你是正在学习51、STM32还是ESP32这个项目的思路和代码框架都具有很高的参考价值。我们不仅会复现功能更会深入探讨一些新手容易忽略的细节比如如何优化显示刷新、如何处理传感器通信中的异常、以及如何让代码结构更清晰易维护。如果你手头正好有类似的硬件不妨跟着一起动手把这个经典的小作品重新“复活”。2. 核心器件选型与工作原理深度剖析一个项目的成功始于对核心器件的透彻理解。我们这个项目主要围绕三块核心芯片展开显示单元12864 LCD、计时单元DS1302、以及感知单元温度传感器。选它们不是偶然而是基于其经典性、易得性和足够的学习深度。2.1 显示核心12864点阵液晶屏12864顾名思义就是横向128个像素点纵向64个像素点。它内部通常由一片液晶驱动控制器如ST7920、KS0108等和液晶面板组成。我们项目里用的很可能是基于ST7920控制器的带字库的型号这也是最常见的一种。为什么是ST7920因为它内置了GB2312简体中文字库16x16点阵和ASCII码字库8x16点阵这意味着我们想显示“温度25℃”这样的中文和符号时不需要自己费力去取模直接发送汉字的内码即可极大地简化了软件开发。它支持并行8位/4位数据和串行三种通信方式为了节省单片机IO口我们通常选择串行模式只需要3根线CS、SID、SCLK。一个关键细节初始化时序。很多新手按照数据手册写初始化代码屏幕却死活不亮问题往往出在电源稳定和复位时序上。ST7920在上电后需要一段稳定时间通常建议延时40ms以上才能接受命令。正确的初始化序列应该是延时等待电源稳定 - 发送功能设置命令选择基本指令集、8位数据接口等- 发送显示开关控制命令开显示、关光标等- 发送清屏命令 - 再次延时。我曾在这里踩过坑少了一次延时导致后续所有命令都无效。2.2 计时核心DS1302实时时钟芯片DS1302是Dallas现Maxim公司生产的一款涓流充电计时芯片它最大的特点是自带一个31字节的静态RAM在主要电源断开时可以由一个纽扣电池通常是3V的CR2032供电保持时间和RAM数据不丢失。它的通信协议值得细说。DS1302采用三线接口CE、I/O、SCLK这是一种同步串行通信。但它的时序比较特殊数据在SCLK的上升沿被写入DS1302在下降沿从DS1302读出。这与常见的SPI时钟相位和极性可调或I2C都不同所以你不能直接用单片机的硬件SPI模块去驱动它必须“位轰炸”Bit-Banging来模拟其时序。写代码时一个字节数据的传输需要先写地址/命令字节最低位LSB在先紧接着是读写的数据字节也是LSB在先。另一个易错点写保护位。DS1302内部有一个写保护寄存器地址0x8E。在每次上电初始化后如果你想写入时间比如校时必须先将写保护寄存器的最高位WP清零否则你对任何时间/日历寄存器的写操作都会被忽略。写完时间后再将其置1以防止误操作修改时间。很多“时间走不准”或“设置无效”的问题根源就在于此。2.3 感知核心温度传感器选型以DS18B20为例原项目压缩包里的温度部分代码可能指向任何传感器比如模拟的LM35或数字的DS18B20。这里我们以更常用、也更复杂的单总线数字温度传感器DS18B20为例进行讲解其思路可以迁移。DS18B20的“单总线”协议是其精髓也是难点。一根数据线既要供电也可外部供电又要完成双向数据传输。协议层包括了复位脉冲、存在脉冲、读写时隙等概念对时序的要求极为严格微秒级的误差都可能导致通信失败。驱动DS18B20的关键在于精确的延时函数。51单片机可以用_nop_()指令循环实现STM32则最好用定时器或系统滴答定时器SysTick来产生精确的微秒延时。例如DS18B20的写“0”时隙要求拉低总线60-120微秒而写“1”时隙则是在拉低总线后1-15微秒内释放总线。如果单片机的主频因为配置不同而变化那么用循环次数实现的延时就会出错这就是为什么别人的代码直接拷过来可能不好用的原因之一。此外注意寄生供电模式下的强上拉。当使用寄生供电数据线同时供电时在启动温度转换命令0x44后需要给数据线提供一个强上拉例如通过一个MOS管接到VCC约750ms以提供足够的电流完成AD转换。如果忽略这一步你可能永远读不到有效的温度数据。3. 系统软硬件架构设计与连接理解了单个器件下一步就是如何将它们有机地组合在一起形成一个稳定工作的系统。这包括硬件电路的连接和软件框架的搭建。3.1 硬件连接图与要点假设我们使用51单片机如STC89C52作为主控以下是典型的连接方式单片机引脚连接至备注P1.0DS1302 CE片选高电平有效P1.1DS1302 I/O双向数据线P1.2DS1302 SCLK串行时钟P1.3DS18B20 DQ单总线数据线需接4.7K上拉电阻P1.512864 SID串行数据线P1.612864 SCLK串行时钟线P1.712864 CS片选低电平有效VCC各模块VCC统一5V供电注意DS1302的备用电池GND各模块GND共地连接注意事项上拉电阻DS18B20的DQ线和DS1302的I/O线通常都需要一个4.7K-10K的上拉电阻接到VCC以确保总线空闲时为高电平。电源去耦在每个芯片的VCC和GND引脚附近最好放置一个0.1uF的陶瓷电容用于滤除高频噪声这对数字电路的稳定运行至关重要尤其是DS1302这种对时序敏感的芯片。DS1302备用电池确保纽扣电池CR2032及其 holder 焊接牢固电池正极接Vcc2负极接GND。在主电源断开时Vcc1电压低于Vcc2芯片自动切换至电池供电。3.2 软件框架与主循环设计软件部分不宜将所有代码堆在main函数里。一个清晰的结构有助于调试和后续功能扩展。建议按模块划分// 伪代码框架 #include config.h // 包含引脚定义、类型重定义等 #include delay.h // 精确延时函数 #include ds1302.h // DS1302驱动 #include ds18b20.h// DS18B20驱动 #include lcd12864.h// LCD驱动 // 全局变量用于在模块间传递数据 struct SystemTime { uint8_t second; uint8_t minute; uint8_t hour; // ... 年、月、日、星期 } currentTime; float currentTemperature 0.0; void main() { // 1. 系统初始化 LCD_Init(); // 液晶初始化包含必要的延时 DS1302_Init(); // 初始化DS1302通常包括清除写保护如需设置时间 // 可以在这里添加一次时间设置代码烧录后注释掉 // DS1302_SetTime(...); // 2. 主循环 while(1) { // 2.1 读取传感器数据频率可较低如每秒1次 DS1302_GetTime(currentTime); currentTemperature DS18B20_ReadTemp(); // 2.2 刷新显示需要时刷新避免频繁全屏刷新导致闪烁 LCD_RefreshDisplay(currentTime, currentTemperature); // 2.3 其他任务如按键扫描用于调时等 Key_Scan(); // 2.4 适当的延时控制主循环频率降低CPU占用 DelayMs(200); } }这个框架的核心思想是“状态采集”“按需刷新”。温度和时间的读取放在主循环中但LCD的刷新需要优化。如果每次循环都清屏重画所有内容屏幕会频繁闪烁体验很差。正确的做法是局部更新只有变化的内容才更新。例如时间每秒变一次我们只重写“秒”所在的区域温度变化较慢可以每5秒或10秒更新一次。这需要在LCD_RefreshDisplay函数内部做逻辑判断。4. 关键驱动代码实现与避坑指南有了框架我们来填充最核心的驱动代码。这里我会给出关键部分的实现思路和代码片段并重点标注那些容易出错的“坑点”。4.1 DS1302驱动代码精讲首先定义引脚和基本操作函数// ds1302.h sbit DS1302_CE P1^0; sbit DS1302_IO P1^1; sbit DS1302_SCLK P1^2; void DS1302_WriteByte(uint8_t dat); uint8_t DS1302_ReadByte(void); void DS1302_WriteReg(uint8_t addr, uint8_t dat); uint8_t DS1302_ReadReg(uint8_t addr);写一个字节的函数是基础// ds1302.c void DS1302_WriteByte(uint8_t dat) { uint8_t i; for(i0; i8; i) { DS1302_SCLK 0; // 先将时钟拉低 DS1302_IO dat 0x01; // 准备数据位LSB first // 这里需要一个小延时确保数据稳定几个_nop_()即可 _nop_(); _nop_(); DS1302_SCLK 1; // 上升沿数据被DS1302锁存 // 再给一个小延时 _nop_(); _nop_(); dat 1; // 移向下一位 } DS1302_SCLK 0; // 最终将时钟拉低回到空闲状态 }坑点1LSB先行。很多新手会习惯性地先发送最高位MSB导致读写数据完全错乱。DS1302协议规定先传最低位。坑点2时序间隔。DS1302_SCLK 1和DS1302_SCLK 0之间的时间以及数据建立时间必须满足数据手册的最小要求通常为数百纳秒。在12MHz的51单片机上一个_nop_()大约是1微秒足以满足。但在更高主频或不同架构的MCU上必须重新校准延时。读一个字节的函数则需要注意I/O口的模式切换uint8_t DS1302_ReadByte(void) { uint8_t i, dat 0; for(i0; i8; i) { dat 1; // 先右移因为最先读到的是LSB DS1302_SCLK 0; _nop_();_nop_(); DS1302_SCLK 1; // 产生上升沿不对 // 注意DS1302在SCLK的下降沿输出数据在上升沿数据变化。 // 因此我们需要在时钟拉高后再读取数据。 _nop_();_nop_(); // 等待数据稳定 if(DS1302_IO) { dat | 0x80; // 如果数据线为高则将最高位置1 } // 循环结束时dat的最高位是第一次读到的数据需要最后整体调整 } // 调整字节顺序使读到的数据正确 // 因为我们是先右移再放高位所以读到的顺序已经反了需要反转一下 // 更清晰的写法是先读低位作为结果的高位 uint8_t temp 0; for(i0; i8; i) { temp 1; if(DS1302_IO) temp | 0x80; DS1302_SCLK 1; _nop_();_nop_(); DS1302_SCLK 0; _nop_();_nop_(); } return temp; }坑点3读时序的理解。这是最混乱的地方。DS1302在SCLK的下降沿之后将数据放到I/O线上并保持有效直到下一个下降沿。因此单片机应该在SCLK为低电平期间读取数据然后在SCLK的上升沿完成一位的读取周期。上面第二种写法是更常见的正确流程拉低时钟 - 延时 - 读取数据位 - 拉高时钟 - 延时。循环8次。4.2 12864液晶串行驱动代码精讲我们使用串行模式驱动ST7920需要模拟其串行时序。// lcd12864.h sbit LCD_SID P1^5; sbit LCD_SCLK P1^6; sbit LCD_CS P1^7; void LCD_WriteByte(uint8_t dat, uint8_t cmd_flag); void LCD_Init(void); void LCD_SetPos(uint8_t x, uint8_t y); // 设置显示位置 void LCD_WriteString(uint8_t x, uint8_t y, char *str);串行写一个字节或命令的函数ST7920的串行协议要求先发送一个5位的同步头“11111”然后是读写标志位RW和寄存器选择位RS最后是8位数据。高位MSB先发。void LCD_WriteByte(uint8_t dat, uint8_t cmd_flag) { // cmd_flag: 1数据, 0命令 uint8_t i; uint16_t send_data 0; // 构造要发送的16位数据 // 高5位固定为11111 // 第6位RW写操作固定为0 // 第7位RS由cmd_flag决定 // 低8位实际数据 send_data 0xF800; // 11111 000 00000000 send_data | ((cmd_flag 0x01) 8); // 设置RS位 send_data | dat; LCD_CS 0; // 使能芯片 for(i0; i16; i) { LCD_SCLK 0; // 发送最高位 if(send_data 0x8000) { LCD_SID 1; } else { LCD_SID 0; } _nop_();_nop_(); LCD_SCLK 1; // 上升沿数据被锁存 _nop_();_nop_(); send_data 1; // 左移发送下一位 } LCD_CS 1; // 关闭片选 // 发送完成后需要等待液晶控制器处理尤其是连续写命令时 DelayMs(1); // 延时1-2ms具体时间参考数据手册 }坑点4同步头与数据构造。务必确保发送的16位数据流格式正确特别是高5位的“11111”同步序列。如果格式错控制器根本不会响应后续数据。坑点5指令执行时间。在发送清屏0x01或地址归位0x02等指令后控制器需要较长时间约1.6ms执行。必须在发送后插入足够的延时DelayMs(2)否则紧接着发送下一条指令会导致控制器忙错误。在初始化序列中这个延时尤为重要。4.3 DS18B20单总线驱动代码精讲DS18B20的驱动最考验时序控制的精确性。// ds18b20.c sbit DQ P1^3; bit DS18B20_Init(void) { bit presence 0; DQ 1; // 先拉高总线 DelayUs(5); DQ 0; // 单片机拉低总线480us以上产生复位脉冲 DelayUs(480); DQ 1; // 释放总线等待15-60us后DS18B20会拉低总线60-240us作为应答存在脉冲 DelayUs(60); presence DQ; // 此时读取如果为0表示器件存在 DelayUs(420); // 等待存在脉冲结束 return (!presence); // 存在返回1 } void DS18B20_WriteByte(uint8_t dat) { uint8_t i; for(i0; i8; i) { DQ 0; // 拉低总线开始写时隙 _nop_();_nop_(); // 拉低后延时约2us满足1us的要求 DQ dat 0x01; // 输出数据位 DelayUs(60); // 保持60us满足写时隙60-120us的要求 DQ 1; // 释放总线 dat 1; // 准备下一位 // 写时隙之间至少需要1us的恢复时间这里用短延时 DelayUs(2); } } uint8_t DS18B20_ReadByte(void) { uint8_t i, dat 0; for(i0; i8; i) { dat 1; DQ 0; // 拉低总线至少1us启动读时隙 _nop_();_nop_(); // 约2us DQ 1; // 释放总线DS18B20将在15us内接管总线输出数据 DelayUs(8); // 等待约8us后数据稳定 if(DQ) { dat | 0x80; // 读到的位是1 } DelayUs(50); // 等待读时隙结束总时长约60us // 注意读时隙最小需要60us我们拉低2us延时8us延时50us总共约60us } return dat; }坑点6延时函数的精确性。上述代码中的DelayUs函数必须非常精确。在51上通常用_nop_()循环实现。但不同的编译优化等级、不同的晶振频率都会影响其实际时间。务必根据你的单片机主频用示波器或逻辑分析仪校准这些延时这是驱动DS18B20成败的关键。坑点7寄生供电下的强上拉。如果你使用寄生供电在发送0x44开始温度转换命令后必须用如下方式提供强上拉void DS18B20_StartConvert(void) { DS18B20_Init(); DS18B20_WriteByte(0xCC); // 跳过ROM命令单设备时 DS18B20_WriteByte(0x44); // 开始转换命令 // 寄生供电下需要强上拉 DQ 1; // 先释放总线 // 通过一个三极管或MOS管将总线强制上拉到VCC持续750ms // 硬件上需要设计软件上这里可以简单延时等待转换完成 DelayMs(750); // 等待转换完成12位精度时最多750ms }5. 功能整合、显示优化与调试心得当各个模块的驱动都调试通过后将它们整合起来并优化显示效果才是一个完整的项目。5.1 时间与温度数据的获取与处理在主循环中我们需要周期性地读取DS1302和DS18B20。但要注意频率DS18B20每次温度转换需要时间最多750ms频繁读取毫无意义且会阻塞主程序。// 定义一些全局变量和标志位 uint8_t second_count 0; uint8_t temp_update_flag 0; void main_loop_tasks(void) { static uint32_t last_temp_read_time 0; uint32_t current_time sys_tick; // 假设有一个系统滴答计数器 // 每秒读取一次时间 DS1302_GetTime(currentTime); // 每5秒读取一次温度 if(current_time - last_temp_read_time 5000) { if(DS18B20_StartConvert()) { DelayMs(800); // 等待转换完成可以优化为非阻塞方式 currentTemperature DS18B20_ReadTemp(); last_temp_read_time current_time; temp_update_flag 1; // 设置温度更新标志 } } // 刷新显示 display_task(); }5.2 显示内容的布局与局部刷新优化12864屏幕可以划分为多个区域。例如我们规划如下布局第1行固定标题“电子时钟 温度计”第2行显示日期格式“2023-10-27 周五”第3行显示时间格式“23:59:59”第4行显示温度格式“温度25.6 ℃”局部刷新的关键只更新变化的部分。我们需要记录上一次显示的内容。// 在显示模块中定义静态变量保存上一次显示的值 static uint8_t last_second 255; // 初始化为一个不可能的值 static float last_temperature -999; void LCD_RefreshDisplay(struct SystemTime *time, float temp) { // 1. 刷新时间秒部分 if(time-second ! last_second) { // 定位到秒的显示位置例如第3行第7列开始 LCD_SetPos(2, 6); // 假设行、列从0开始计数 printf_lcd(%02d, time-second); // 需要实现LCD格式输出函数 last_second time-second; // 如果秒变了分和时也可能变在59秒跳转时但为了简化这里只更新秒。 // 更完善的方案是检查时、分、秒任何一个变化就重写整个时间字符串。 } // 2. 刷新温度当温度变化超过0.1度或标志位被设置时 if(temp_update_flag || fabs(temp - last_temperature) 0.1) { LCD_SetPos(3, 3); // 定位到温度值显示位置 printf_lcd(%5.1f, temp); // 显示5位字符其中一位小数 last_temperature temp; temp_update_flag 0; } // 日期和星期通常一天变一次可以在每天0点触发一次全更新这里省略。 }优化点printf_lcd需要自己实现因为标准printf通常不支持浮点数在51上。对于温度显示可以先将浮点数乘以10转换成整数然后分别显示整数部分和小数部分。5.3 项目调试中的常见问题与解决思路屏幕不亮或乱码检查电源和背光确保VCC和背光引脚LED LED-供电正确。有些屏幕背光需要串联限流电阻。检查初始化序列和延时这是最常见的问题。严格按照数据手册的时序并确保每次命令后有足够的延时。用逻辑分析仪抓取CS、SID、SCLK的波形与手册对比。检查对比度电压V0有些模块需要外接电位器调节对比度电压不合适会导致全黑或全白。DS1302时间不走或读写出错检查写保护位确保设置时间前已清除写保护向0x8E写0x00。检查备用电池用万用表测量电池电压是否正常2.5V。主电源断开后时间是否还能保持用逻辑分析仪抓取时序对照数据手册看CE、SCLK、IO的波形是否符合要求特别是上升沿/下降沿与数据的关系。DS18B20读不到数据或数据全为0xFF/0x00检查上拉电阻必须接一个4.7K的上拉电阻到VCC。精确校准延时函数这是重中之重。用示波器测量DQ线在复位、读、写时的波形看时间参数是否在DS18B20要求的范围内。检查电源模式如果使用寄生供电在温度转换命令后是否提供了强上拉可以改用外部供电VCC接5VGND接地来排除这个问题。尝试“跳过ROM”命令单总线上只有一个器件时使用0xCC命令跳过ROM搜索可以简化流程。显示闪烁避免全屏刷新采用上面提到的局部刷新策略。优化主循环频率在主循环末尾增加一个合理的延时如DelayMs(50)避免刷新过快。功耗问题如果考虑低功耗可以在主循环中让单片机进入空闲模式Idle Mode用DS1302的闹钟输出或秒中断来唤醒单片机完成读取和显示后再次休眠。这样可以极大地降低系统平均功耗。回顾这个“LCD.rar”项目它虽然简单但涵盖了嵌入式开发从硬件连接到软件驱动、从时序调试到功能整合的完整流程。每一个坑点背后都是对数据手册的深入理解和对硬件行为的细致观察。当你成功让屏幕稳定地显示出正确的时间和温度时所获得的成就感远大于仅仅复制一段代码。希望这份详细的复盘能帮助你不仅复现这个项目更能理解其背后的原理从而具备独立开发类似小型嵌入式系统的能力。本文还有配套的精品资源点击获取
返回列表