简介
这是常用的单色液晶 LCD 显示屏。
- 型号为 LM3033DFW(深圳拓普微)
- 5V 单电源供电(3.3V不可以,对比度会降低到看不清)
- 支持并口(8080时序)和串行通讯(SPI)
- 带字库
框图为:
对外通过 20 Pin 排针提供接口:
并行接口原理图:
串行接口原理图:
对于 CS
引脚,绝大多数情况下都建议用 I/O 口控制。这里之所以直接拉高,是因为数据手册中有明确说明。
建议在将 CS 引脚设置为
低电平
前,将 SCLK 引脚保持为低电平
,将 SID 引脚保持在最后一个电平状态
。对于只有一个 ST7920 和一个 MPU 的最小系统,只需要 SCLK 和 SID 引脚。 CS 引脚应拉高。
电器参数
直流特性
拓普威厂家提供的整机数据手册:
ST7920芯片厂家提供的数据手册:
这些参数有什么用?
2021 年 12 月,车间反馈其生产的 某设备,出现大量的花屏现象,故障率高达 20% 以上。
- 排除程序初始化问题
- 排除接插件涂三防漆接触不良问题
- 使用示波器观察 SPI 通讯波形,波形时序正确
LCD 是 5V 供电,但单片机使用 3.3V 供电,所以 LCD 的 SPI 通讯接口实际是使用 3.3V 驱动的。
根据手册可知,若 5V 供电,SPI 通讯接口需要的高电平为 0.7 * VDD
= 3.5V,单片机引脚最高输出 3.3V ,已经不符合数据手册要求。而经过测量,发生故障的 LCD 供电电压实际达到了5.36V,即显示屏至少需要 3.75V 才能可靠的识别为高电平。换句话说,单片机发送的高电平,LCD 已经不能识别了,所以导致了花屏现象。
经过批量测试,这一批电源的 5V 输出,实际电压在5.15 V ~ 5.38 V 之间,而大于 5.3 V 后,就容易花屏。
所有花屏的设备,将 LCD 供电电压降到 4.8V 后,都恢复了正常。
5V 输出电压为什么波动这么大?
5V 输出电源是使用 LM2674
芯片设计的,其中确定输出电压的电阻精度使用的是 5%精度,这导致大批量生产时,输出电压偏差较大。
显示屏主控
主控采用了 ST7920。驱动编程需要仔细研读该芯片数据手册,下面结合具体例子看一下数据手册中的重点内容。
重复代码有大作用?
2010 年我接手了一个项目,里面有一段代码比较奇怪:对 LCD 的初始化之后,连续调用了 4 次显示内容函数 WriteTextScreen
:
// LCD 初始化initLCDM();SdCmd(0x30); // 8bit I/F, basic command, graphic offSdCmd(0x01); // clr text screen // 显示内容,注意这里连续调用了四次相同的函数WriteTextScreen(dispLCD);WriteTextScreen(dispLCD);WriteTextScreen(dispLCD);WriteTextScreen(dispLCD);
我觉得这样写不对,就去掉了其中 3 个,结果显示立马异常:会花屏!恢复后,显示正常。
离奇!
根据温伯格离奇定律:
如果觉得一件事情离奇,大概率只是你眼界不够。
我决定到数据手册中寻找原因。通过阅读数据手册发现,当 LCD 主频为 540KHz 时,清屏命令(0x01)需要 1.6ms 的执行时间。
而上面的代码在发出清屏命令 SdCmd(0x01)
后,紧接着就调用了显示函数,中间没有任何延时,这就导致显示函数发送的数据没有得到完全执行,从而引起了花屏现象。
指令的执行需要时间!
向 ST7920 传输多个指令/数据时,**必须考虑指令执行时间。MPU 必须等待上次指令完成,才能发送下一条指令。**ST7920 内部没有指令缓冲区。(Datasheet - Serial Interface P26)
花屏
某年的11月份,为了降低 LCD 刷屏时间,我将 LCD SPI 接口的速率由 400KHz 提高到了 800KHz。程序通过了自测、中式,然后下发给车间生产。几个月之后,车间负责人打电话说,发现这批次的 LCD 屏幕有小概率会花屏。他们测试了 50 块显示屏,其中有 6 块会花屏。
进一步测试发现,花屏与温度有关,现在是夏天,室温达到了30多摄氏度,出现了花屏。
温度影响了什么地方,导致花屏呢?
根据上面的例子,我们已经知道了一个可能:LCD 显示屏来不及执行指令,可能会花屏。
设计程序验证假设:找到一个会花屏的 LCD,保持 SPI 主频不变的情况下,在指令与指令之间增加延时,发现花屏现象消失,即便放到 60℃ 的高温箱也不会花屏,但只要去掉指令与指令之间的延时,就会花屏。
这验证了我们的假设,即指令来不及执行。
这说明 LCD 屏的主频有了较大的波动。查看数据手册,主频大小由 OSC1 和 OSC2 引脚间的电阻决定,其关系为:
LM3033 显示屏使用的电阻是 24K 欧姆,根据电阻丝印推测是 5% 精度。
这个电阻会随温度和批次变化,温度升高,电阻阻值变大(金属薄膜电阻),当电阻实际值比 24K 大时,LCD 的主频会降低,执行指令的时间会增加,这样就会造成 LCD 屏来不及执行指令而产生花屏。
令人震惊的耗时
写满屏汉字大约多少时间?
一次非规程序更新之后,客户反馈只要操作菜单界面,设备就会断电!断电是因为设备判断接入的传感器发生了故障,传感器故障会引起分站断电。
实际测试发现,传感器通讯数据正常,是设备误判了。之所以会误判,是因为在菜单中某个函数运行很耗时,导致处理传感器数据时,已经触发了通讯超时条件。之前之所以没有问题,是因为之前要连续发生 3次 超时,才认为是真的发生超时故障,而这个非规程序为了提高故障断电响应速度,将 3 次判断条件改为了 1 次。
测量了菜单中涉及的函数,发现显示一行汉字只需要 0.88ms。
但是将焦点反白下移一行(上图黑底区域),需要 138ms!
这时间耗在哪里?是怎么耗掉的?
启动传输时,需要先发送一个起始字节,用于同步和指示读/写和选择寄存器/数据。后续的每 1 个字节都被分成 2 部分传输:高 4 位和低 4 位。
所以每发送 1 字节命令或数据, 实际需要发送 3 字节!
一般的芯片都有块写和块读功能,即设置一个起始地址后,就可以一直写数据或者读数据。而这个芯片则是每 1 个字节都要分成 3 字节传输,没有第二个选项。
怪异的显示
代码:
uint8_t full_screen_buf[] = {"这是测试第一行 ""这是测试第二行 ""这是测试第三行 ""这是测试第四行 "
};
WriteTxtAny(1, 1, full_screen_buf); //满屏显示
得到的显示结果却是:
???
原因在于主控最多只能显示 16 个汉字 x 2 行,但被按照 8 个汉字 x 4 行来显示。
LCD 显示范围是 16 汉字 x 2 行。
这种显示方式,给我们编程带来了麻烦,但是硬件容易实现啊,又不是不能用,你们凑合一点吧。
莫名其妙的乱码
代码:
WriteTxtAny(2, 1, "123我会显示乱码");
代码执行结果是:
???
原因是 CGRAM 字体和 CGROM 字体只能显示在每个地址的起始位置!
奇怪的地址表示(现有驱动)
代码:
con_disp_16(0x81, 0x90, 2, 1); //第2行第2列反白2个汉字区域
con_disp_16(0x8D, 0x80, 2, 1); //第3行第6列反白2个汉字区域
是为了在 LCD 上显示两处反白:
道理我都明白,可这一堆数字是什么鬼???
我只是想做个显示界面而已!!!
要想明白 0x81
、0x8F
、0x8D
、0x80
,需要看手册关于图形 RAM 的设置。
现有的驱动程序:
与具体硬件耦合在一起,不是模块化代码,移植不方便
代码中有不明意义的数字(魔术数):
LCD_SSP->DR = 0xF8;
LCD_SSP->DR = 0xFA;
write_com(0x01);
write_com(0x06);
write_com(0x0C);
write_com(0x30);
write_com(0x34);
write_com(0x36);
这些垃圾代码是从哪里来的?
答案是:拓扑微官网 提供的范例。
ST7920 模块化代码
编译器需使能 -C99
代码结构:
其中:
lcd_func.h
:模块的接口,对外提供的 API 函数lcd_func.c
:接口的实现,依赖的函数(以弱引用修复符__weak
修饰)lcd_func_cfg.h
:默认配置和跨编译器相关特性
需要一个用户配置头文件 app_lcd_func_cfg.h
,用于根据自身应用特性来自定义配置和编译器相关特性。
使用方法:
- 查看模块代码是否是只读属性,如果不是,将它们设置为只读的。
- 将模块化代码加入工程
- 编写硬件相关驱动(函数名和参数已预先指定)
- 设置用户配置参数
- 调用模块 API 函数,完成指定功能
模块只读属性,意味着用户不能修改模块,如果想添加新特性或者修改 BUG ,需向模块维护者提交申请。
驱动包给出了一个在 Keil 平台下的用户配置文件 app_lcd_func_cfg.h
,就连相关的硬件驱动,我也给出了公司内部常用 CPU 驱动代码,包括硬件 SPI 和软件 SPI, 可以说是贴心的喂到了嘴边!
1.意义不明的魔数
修改前:
LCD_SSP->DR = 0xF8;
LCD_SSP->DR = 0xFA;write_com(0x01);
write_com(0x06);
write_com(0x0C);
write_com(0x30);
write_com(0x34);
write_com(0x36);
修改后,用合理命名的宏来代替魔数,用合理命名的函数名来描述行为功能,遵循代码自描述原则:
#define SERIAL_MODE_WRITE_INSTRUCTION_CODE 0xF8
#define SERIAL_MODE_WRITE_DATA_CODE 0xFA
lcd_send_data(SERIAL_MODE_WRITE_INSTRUCTION_CODE);
lcd_send_data(SERIAL_MODE_WRITE_DATA_CODE);clear_ddram();
set_ac_direction(LCD_AC_INCREASED);
display_control(LCD_DESPLAY_ON, LCD_CURSOR_OFF, LCD_BLINK_OFF);
enable_basic_instruction();
enable_extended_instruction();
graphic_display_on();
2.满屏显示例子
修改前:
WriteTxtAny(1, 1, full_screen_buf); //满屏显示
修改后:
disp_txt_by_specify_location(LCD_ROW_1, LCD_COLUMN_1, full_screen_buf);
修改后的代码更清晰,显示所见即所得,比如上面那个满屏显示的例子:
uint8_t full_screen_buf[] = {"这是测试第一行 ""这是测试第二行 ""这是测试第三行 ""这是测试第四行 "
};
原来代码第二行和第三行在显示屏上是颠倒的,但是用新的驱动程序,显示和代码是一致的,而且可以实现自动换行的功能。模块内部将原驱动怪异的显示进行了修正,将复杂处理隐藏起来,给出的接口是易用的、不违反常规的。
3.反白例子
修改前:
con_disp_16(0x81, 0x90, 2, 1); //第2行第2列反白2个汉字区域
con_disp_16(0x8D, 0x80, 2, 1); //第3行第6列反白2个汉字区域
修改后,统一坐标系,隐藏了不必要的地址信息,提供简洁的 API 接口:
disp_backlight(LCD_ROW_2, LCD_COLUMN_2, 2); //第2行第2列反白2个汉字区域
disp_backlight(LCD_ROW_3, LCD_COLUMN_6, 2); //第3行第6列反白2个汉字区域
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)