资讯详情

资讯详情

基于STC89C52与12864液晶屏的DS1302时钟及DHT11温湿度监测系统设计

1. 项目缘起与整体设计思路1.1 为什么选择12864这款屏12864液晶屏在嵌入式圈子里算是老面孔了几乎每个玩单片机的人都跟它打过交道。它的核心参数很明确128列×64行像素点阵内置字库芯片支持并行和串行两种通信方式。我选它做这个项目理由其实很朴素——手头正好有一块而且它自带中文字库显示个“温度”“湿度”“时钟”这类汉字不需要额外做字模取模省事。市面上常见的12864模块分两种主控方案ST7920和KS0108。ST7920自带字库支持SPI串行模式接线少适合引脚资源紧张的场景KS0108是纯点阵屏不带字库需要自己取模但刷新速度更快。这个项目我用的ST7920方案因为串行模式只需要三四根线就能驱动把省下来的IO口留给温湿度传感器和时钟芯片。从显示需求倒推12864的64行像素可以分成上下两半各32行上半屏显示时钟下半屏显示温湿度布局上天然合适。如果换成1602或者2004这种字符屏虽然也能显示但没法做图形化界面比如画个温度计图标、显示个湿度进度条之类的视觉效果差很多。12864的像素级控制能力让界面设计有了更多余地。1.2 系统架构的取舍整个系统我拆成了三个独立的功能模块时钟模块、温湿度采集模块、显示模块。这三个模块通过主控芯片协调工作主循环里轮询采集数据、刷新屏幕。时钟部分我用了DS1302这是一款经典的实时时钟芯片走三线SPI接口带备用电池引脚掉电后时间不丢。有人会问为什么不用单片机内部的RTC原因很简单——很多常用单片机内部RTC精度太差一天能差好几分钟而且掉电就归零。DS1302外挂一个纽扣电池精度虽然也一般但至少掉电不丢时间日常使用足够了。如果对精度要求更高可以换DS3231内置温补晶振年误差能控制在几分钟以内但价格贵一些。温湿度采集我选了DHT11。这个传感器在爱好者圈子里争议挺大——有人说它精度差、响应慢但我觉得要看场景。室内环境监测温度精度±2℃、湿度精度±5%RH完全够用而且它单总线接口一根数据线就能读数据接线极其简单。如果要做工业级监测那肯定得上SHT30或者SHT31I2C接口精度高一个档次但成本也上去了。这个项目定位是桌面级应用DHT11够用。主控芯片我用的是STC89C5251内核资源刚好够用8KB Flash存程序512字节RAM跑变量32个IO口分配下来绰绰有余。有人可能觉得51太老了但做这种小项目51的生态成熟、资料多、烧录方便出了问题好排查。用STM32当然性能更强但对这个项目来说属于杀鸡用牛刀。1.3 显示布局的设计考量12864的屏幕分辨率是128×64我把它分成三个区域来用。最上面一行显示日期和星期中间用大号字体显示时间时:分:秒下面两行分别显示温度和湿度。这里有个细节需要注意ST7920内置的字库是16×16点阵的汉字和8×16点阵的ASCII字符。如果时间用8×16的ASCII字符显示一行能显示16个字符但看起来太小。我实际测试下来用16×16的点阵显示数字视觉效果最好——每个数字占两个ASCII字符的宽度一行显示“12:34:56”刚好占满中间区域清晰醒目。温度和湿度的显示我加了一点小设计温度值后面跟一个自定义的“℃”符号湿度值后面跟一个“%”符号。这两个符号在ST7920的字库里没有现成的需要自己取模写入CGRAM。取模的时候要注意16×16的点阵需要32个字节的数据取模软件设置成“纵向取模、字节倒序”模式跟ST7920的显示RAM格式匹配。注意ST7920的CGRAM只能存4个自定义字符每个16×16如果自定义符号超过4个就需要用绘图模式直接写GDRAM那样更灵活但代码量会大不少。2. 核心硬件选型与接线要点2.1 主控与最小系统STC89C52的最小系统包括晶振电路、复位电路和电源滤波。晶振我用的是11.0592MHz这个频率在串口通信时能产生精确的波特率虽然这个项目没用到串口但习惯性选它方便后续扩展调试。复位电路是经典的10uF电容加10K电阻上电复位时间常数约100ms足够单片机完成初始化。电源部分要特别注意12864背光LED的电流一般在20mA左右加上单片机本身和传感器的功耗整个系统峰值电流大概在60-80mA。如果用USB供电没问题但如果用电池供电建议加一个100uF的电解电容做储能避免背光开启瞬间电压跌落导致单片机复位。2.2 12864的接线方式ST7920支持并行8位、并行4位和串行三种模式。我选串行模式因为接线简单只需要5根线CS片选、SID数据、CLK时钟、VCC、GND。背光的话A接正极、K接负极中间串一个100Ω的限流电阻。串行模式的时序是这样的每次传输一个字节高位在前在CLK的上升沿数据被锁存。发送指令之前要先发一个字节的同步头指令是0xF8数据是0xFA。这个同步头的设计是为了区分指令和数据刚开始用的时候容易搞混记住“发指令用F8发数据用FA”就行。实际接线的时候SID和CLK两根线尽量短如果超过10厘米建议在CLK线上串一个33Ω的电阻做阻抗匹配减少信号反射。我一开始用杜邦线接了20厘米屏幕偶尔会出现花屏后来把线剪短到8厘米就稳定了。2.3 DS1302时钟芯片的注意事项DS1302的三根信号线是CE使能、IO数据、SCLK时钟。它跟单片机通信是半双工的IO线既要发数据又要收数据所以单片机端需要把IO口配置成开漏输出或者准双向口。51单片机的P0口是开漏的正好适合但需要外接上拉电阻我用的是4.7K。备用电池我用的是CR2032纽扣电池标称电压3V。DS1302的工作电压范围是2.0V到5.5V3V电池直接接VBAT引脚就行。这里有个坑如果主电源和备用电池同时供电DS1302会自动选择电压高的那一路。但如果你用的电池电压超过3.3V而主电源是5V那电池就永远用不上。所以电池电压要低于主电源电压3V的纽扣电池配5V主电源刚好。晶振用的是32.768kHz的圆柱体晶振这个频率是2的15次方经过15级分频正好得到1Hz的秒信号。晶振的负载电容一般是6pF但实际布线时杂散电容就有几个pF所以外接的匹配电容通常选6-12pF。我试过不接匹配电容走时一天快十几秒后来加了两个10pF的电容一天误差降到两秒以内。2.4 DHT11的读取时序DHT11是单总线器件一根数据线完成所有通信。它的时序对时间要求比较严格主机先拉低总线至少18ms然后释放DHT11会拉低80us作为响应再拉高80us之后开始传输40位数据。每一位数据的格式是50us低电平起始然后高电平持续26-28us表示“0”持续70us表示“1”。用51单片机读的时候因为主频是11.0592MHz一个机器周期约1.085us所以需要精确的延时函数。我实测下来用_nop_()空指令做微秒级延时最可靠循环延时容易受编译器优化影响。读取间隔要注意DHT11的采样周期不能短于1秒否则数据会不准。我一开始在主循环里每200ms读一次结果湿度值一直跳后来改成2秒读一次就稳定了。另外DHT11上电后需要1秒的稳定时间跳过第一次读数。3. 软件架构与关键代码实现3.1 主循环的任务调度整个程序没有用操作系统就是一个大的while(1)循环里面按顺序执行三个任务读时钟、读温湿度、刷新显示。这种前后台架构在简单项目里够用但要注意任务的时间分配。我的做法是用定时器0产生2ms的定时中断在中断里维护一个软件计数器。主循环里判断计数器是否达到阈值达到了就执行对应任务。比如时钟每500ms读一次温湿度每2秒读一次显示每200ms刷新一次。这样各任务的执行频率独立互不干扰。这里有个经验显示刷新不要太频繁200ms一次足够了人眼根本感觉不到闪烁。如果刷太快比如50ms一次屏幕反而会有轻微的水波纹感而且占用大量CPU时间。我试过10ms刷新一次结果温湿度读取的时序都被打乱了因为DHT11的读取需要关中断刷新太频繁会导致关中断次数过多影响其他任务的实时性。3.2 DS1302的读写驱动DS1302的寄存器是BCD码格式的读写的时候需要做BCD和十六进制之间的转换。比如秒寄存器的值0x59表示59秒而不是十进制的89。写驱动的时候我封装了两个函数一个把十进制转BCD一个把BCD转十进制。unsigned char DecToBcd(unsigned char dec) { return ((dec / 10) 4) | (dec % 10); } unsigned char BcdToDec(unsigned char bcd) { return ((bcd 4) * 10) (bcd 0x0F); }读写时序上DS1302是低位先传。写一个字节的流程是CE拉高先发命令字节包含地址和读写位然后发数据字节每个bit在SCLK的上升沿被DS1302采样。读的时候类似只是在发完命令字节后DS1302会在SCLK的下降沿输出数据。初始化的时候要注意DS1302的秒寄存器最高位是CHClock Halt位上电后默认是1表示时钟停止。所以初始化时必须先写0清除这一位时钟才会开始走。我一开始忘了这一步读出来的时间一直不变排查了半天才发现是CH位没清。3.3 DHT11的读取函数DHT11的读取函数需要严格遵循时序中间不能被打断。所以我在读取之前先关总中断读完再开。整个读取过程大概4ms左右对系统其他任务的影响可以接受。unsigned char DHT11_Read(void) { unsigned char i, j, temp; unsigned char data[5] {0}; // 主机发送起始信号 DHT11_IO 0; Delay_ms(20); DHT11_IO 1; Delay_us(30); // 等待DHT11响应 if (!DHT11_IO) { while (!DHT11_IO); // 等待80us低电平结束 while (DHT11_IO); // 等待80us高电平结束 // 读取40位数据 for (i 0; i 5; i) { for (j 0; j 8; j) { while (!DHT11_IO); // 等待50us低电平结束 Delay_us(40); data[i] 1; if (DHT11_IO) { data[i] | 1; while (DHT11_IO); // 等待剩余高电平结束 } } } } // 校验数据 if (data[4] (data[0] data[1] data[2] data[3])) { return 1; // 读取成功 } return 0; // 校验失败 }这段代码里有个细节判断数据位是0还是1我是在50us低电平结束后延时40us再采样。因为0的高电平只有26-28us40us后已经变低了而1的高电平有70us40us时还是高的。这个40us的延时是经过实测的用示波器抓过波形确认。3.4 12864的串行驱动ST7920的串行驱动核心是两个函数写指令和写数据。两者的区别只在同步头字节不同。void LCD_WriteByte(unsigned char dat, unsigned char mode) { unsigned char i; LCD_CS 1; LCD_CLK 0; // 发送同步头 SendByte(mode ? 0xFA : 0xF8); // 发送数据高4位 SendByte(dat 0xF0); // 发送数据低4位 SendByte((dat 4) 0xF0); LCD_CS 0; } void SendByte(unsigned char byte) { unsigned char i; for (i 0; i 8; i) { LCD_SID (byte 0x80) ? 1 : 0; LCD_CLK 1; byte 1; LCD_CLK 0; } }注意ST7920串行模式下一个字节要分两次发先发高4位再发低4位。每次发送都是高位在前。我一开始没注意这个直接把整个字节发出去结果屏幕完全没反应。后来查数据手册才发现串行模式下每个字节只传4位有效数据需要分两次传输。显示字符串的时候先设置DDRAM地址然后连续写数据就行。ST7920的DDRAM地址跟屏幕位置对应关系是第一行0x80-0x87第二行0x90-0x97第三行0x88-0x8F第四行0x98-0x9F。注意第三行和第四行的地址不是连续的中间隔了一段这是ST7920的设计特点。4. 调试过程中踩过的坑与解决方案4.1 屏幕花屏与乱码问题花屏是12864调试中最常见的问题原因通常有三个接线太长、时序太快、初始化不对。我第一次点亮屏幕的时候显示的全是乱码。排查过程是这样的先用万用表确认VCC和GND没有接反电压5V正常然后用示波器看CLK和SID波形发现上升沿不够陡峭有明显的振铃。把杜邦线从20厘米换成8厘米后波形干净了很多但还有零星乱码。接着检查初始化代码发现我漏掉了“功能设定”指令的第二次发送。ST7920的初始化流程要求先发0x30基本指令集延时再发0x30再延时然后发0x0C开显示、关光标最后发0x01清屏。我少发了一次0x30导致屏幕一直处于不确定状态。补上之后显示就正常了。还有一个隐蔽的坑ST7920在串行模式下每次发送指令或数据之间需要至少72us的间隔。我一开始连续发送没有加延时屏幕偶尔会丢指令。后来在每个SendByte之后加了100us的延时问题彻底解决。4.2 温湿度读数跳变DHT11的读数跳变是另一个让人头疼的问题。表现是温度值偶尔跳到0或者湿度值突然变成95%。排查下来有几个原因第一个原因是读取间隔太短。前面说过DHT11的采样周期不能短于1秒我改成2秒后跳变频率明显降低。第二个原因是电源干扰。DHT11和12864共用5V电源背光开启时电流突变会通过电源线耦合到DHT11的数据线上。我在DHT11的VCC和GND之间并了一个100nF的陶瓷电容数据线上串了一个100Ω的电阻问题基本消失。第三个原因比较隐蔽DHT11对湿度敏感如果传感器附近有热源比如单片机或者稳压芯片读数会偏高。我把DHT11用排线引出远离主板5厘米以上湿度读数就准了。实测下来DHT11的湿度精度在±5%RH以内温度精度在±1℃以内日常使用完全够。4.3 时钟走时不准的排查DS1302走时不准通常跟晶振和匹配电容有关。我一开始用的晶振是从旧板子上拆的走时一天快十几秒。换了新的32.768kHz晶振匹配电容用10pF一天误差降到2秒左右。还有一个容易被忽略的点DS1302的晶振引脚对杂散电容很敏感。如果晶振引脚走线太长或者靠近其他信号线走时精度会受影响。我把晶振直接焊在DS1302的引脚上尽量缩短走线效果好了很多。另外DS1302的涓流充电寄存器如果配置不当会导致备用电池一直处于充电状态电池寿命会缩短。如果用的是不可充电的CR2032一定要确保涓流充电功能是关闭的。我检查了寄存器0x90的值确认是0x00没有开启充电功能。4.4 常见问题速查表现象可能原因排查方法解决方案屏幕全黑背光未接或限流电阻过大测背光引脚电压检查背光接线减小限流电阻屏幕全亮无内容对比度电压不对调VR引脚电压调整对比度电位器显示乱码初始化时序不对检查初始化指令序列补全初始化流程加延时显示花屏接线过长或时序过快示波器看波形缩短接线降低时钟频率温度读数跳变读取间隔太短或电源干扰延长间隔加滤波电容2秒读一次加100nF电容湿度读数偏高传感器靠近热源手摸传感器附近温度传感器远离发热元件时钟走时快晶振匹配电容偏小换不同容值电容测试增大匹配电容至10-12pF时钟走时慢晶振匹配电容偏大换不同容值电容测试减小匹配电容至6-8pF掉电后时间归零备用电池没电或未接测VBAT引脚电压更换电池检查电池座数据校验失败时序被打断检查中断是否关闭读取期间关总中断5. 显示界面的优化与个性化5.1 自定义字符的取模方法ST7920支持4个16×16的自定义字符存在CGRAM里。要显示“℃”和“%”这两个符号需要先用取模软件生成点阵数据。取模软件的设置很关键点阵大小选16×16取模方式选“纵向取模”字节倒序选“是”。生成的数据是32个字节前16个字节是上半部分后16个字节是下半部分。写入CGRAM的时候地址从0x40开始每个自定义字符占32个字节。// 写入自定义字符“℃” void Write_Custom_Char(void) { unsigned char i; LCD_WriteCmd(0x40); // CGRAM起始地址 for (i 0; i 32; i) { LCD_WriteData(custom_char_celsius[i]); } }显示的时候用0x00到0x03这四个编码来调用自定义字符。比如在温度值后面显示“℃”就写LCD_WriteData(0x00)。5.2 界面布局的调整技巧12864的显示区域是128×64我把它分成四行来用。第一行显示日期和星期第二行显示时间第三行显示温度第四行显示湿度。每行16个汉字的位置但实际显示的时候不需要占满。时间的显示我用了大号字体每个数字占2个字符宽度这样“12:34:56”占14个字符宽度居中显示在第二行。温度和湿度用普通大小前面加一个图标或者标签看起来更直观。这里有个小技巧ST7920支持“显示移位”指令可以让整个屏幕内容左右移动。我试过用这个指令做滚动效果但发现移位后地址会乱显示内容会错位。后来放弃了滚动效果改用静态布局稳定可靠。5.3 背光控制与功耗优化12864的背光电流在20mA左右如果一直开着整个系统的功耗大概在80mA。对于电池供电的场景这个功耗偏大。我的做法是用一个NPN三极管控制背光的通断单片机的一个IO口通过1K电阻接三极管基极。背光控制可以做成自动模式如果环境光线暗就开背光如果光线亮就关背光。加一个光敏电阻就能实现。不过这个项目我没做自动控制而是用了一个简单的策略按键按下后开背光30秒无操作自动关背光。这样既省电又不影响使用。关背光的时候要注意背光关闭后液晶本身的显示还在只是没有背光看不清。如果完全不需要显示可以把ST7920也进入睡眠模式功耗能降到微安级。但睡眠模式唤醒后需要重新初始化比较麻烦这个项目没做。6. 项目扩展与后续改进方向6.1 增加数据记录功能现在的系统只是实时显示没有历史数据。如果要增加数据记录可以外挂一个AT24C02 EEPROM每半小时存一次温湿度数据存满后覆盖最旧的数据。AT24C02是I2C接口两根线就能驱动容量2KB能存几百条记录。读取历史数据的时候可以在屏幕上做一个简单的曲线图。12864的64行像素正好可以画一个趋势图横轴是时间纵轴是温度或湿度。画图的时候用“绘图模式”直接写GDRAM比字符模式灵活得多。6.2 添加报警功能温湿度超出设定范围时报警这个功能很实用。硬件上加一个有源蜂鸣器单片机的一个IO口控制。软件上设定阈值比如温度高于30℃或低于10℃就报警湿度高于80%或低于20%也报警。报警的时候可以让屏幕闪烁或者显示一个醒目的警告图标。蜂鸣器响的时候要注意有源蜂鸣器需要20mA左右的驱动电流51单片机的IO口直接驱动可能不够建议加一个三极管做电流放大。6.3 无线数据传输的可行性如果想把数据传到手机或者电脑上可以加一个蓝牙模块或者WiFi模块。蓝牙模块用串口跟单片机通信把温湿度数据打包发送。手机端写一个简单的APP接收显示或者用现成的串口调试工具也行。不过加无线模块后功耗会明显增加蓝牙模块工作电流在20mA左右WiFi模块更高可能到100mA以上。如果还是电池供电续航会大幅缩短。所以这个扩展更适合有固定电源的场景。6.4 外壳设计与安装方式裸板运行总归不太美观我后来用3D打印做了一个简单的外壳。外壳分前后两半前面板开一个12864的窗口后面板开一个USB供电口和传感器引出口。DHT11用排线引出到外壳外面避免机箱内部温度影响测量。安装方式上我用了四个铜柱把主板固定在外壳内部屏幕用热熔胶固定在前面板上。整体尺寸控制在10cm×8cm×3cm左右放在桌面上不占地方。如果不想做外壳用亚克力板加铜柱搭一个简易支架也行成本更低。7. 个人实操心得与避坑建议7.1 焊接与布线的心得12864的排针间距是2.54mm焊接的时候用尖头烙铁温度调到350℃左右。焊锡不要太多否则容易连锡。我习惯先用松香助焊焊完后用酒精清洗板面看起来干净也能防止残留物导致漏电。布线方面信号线和电源线尽量分开走。如果用的是洞洞板电源线用粗一点的镀锡铜线信号线用细线。晶振的走线要尽量短下面不要走其他信号线避免干扰。7.2 代码调试的实用技巧调试嵌入式代码串口打印是最有效的手段。虽然这个项目最终没用到串口但调试阶段我接了一个USB转TTL模块把关键变量的值打印出来。比如DS1302读出来的原始BCD码、DHT11读到的40位数据打印出来一看就清楚问题在哪。另外养成写注释的习惯。特别是时序相关的代码把每个延时的时间标注清楚过几个月再回头看也能快速理解。我还会在关键位置加一些LED指示灯比如读取成功亮绿灯失败亮红灯一眼就能看出系统状态。7.3 元件选购的注意事项12864模块市面上有很多版本价格从十几块到几十块不等。便宜的模块往往用的是拆机屏或者次品屏显示效果差有坏点。建议买带PCB底板的模块这种模块集成了升压电路和对比度调节接线简单稳定性好。DHT11也有仿品精度和稳定性都不如原厂。辨别方法是看外壳上的字迹是否清晰引脚是否平整。仿品的引脚往往有氧化痕迹字迹模糊。如果预算允许直接上DHT22或者SHT30省心很多。DS1302的晶振一定要买新的拆机晶振的频率偏差可能很大。匹配电容用瓷片电容不要用电解电容电解电容的温漂大会影响走时精度。7.4 关于项目周期的建议这个项目从画图到调试完成我用了大概三个周末。第一个周末画原理图和PCB第二个周末焊接和写驱动第三个周末调试和优化。如果你也想做类似的项目建议留出足够的调试时间因为时序问题往往需要反复测试才能解决。不要想着一次成功嵌入式开发就是这样遇到问题解决问题慢慢就熟练了。我一开始连12864的初始化都搞不定屏幕亮了但全是乱码后来查资料、问论坛、用示波器看波形一点点排查最后搞定的时候还是挺有成就感的。这个项目后续还可以这样扩展把温湿度数据上传到云平台用手机远程查看或者加一个SD卡模块把数据存成CSV文件方便用Excel分析。硬件上预留了串口和几个空闲IO口扩展起来很方便。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →