如何用单片机直接驱动LCD液晶屏
LED数码管的驱动是比较简单也容易理解的,多位数码管一般是LED阵列的形式,每个数字使用一个公共端,不同数字的对应同笔段使用一个控制端;驱动采用分时扫描没个数字位,动态显示。但是LED比较费电,我想做一个用电池供电的钟,用发光管电池就撑不了多久了。于是我考虑用液晶。
在这边的电子市场我买到一个4位笔段式液晶屏,4个数字最中间有冒号,边上还有几个箭头符号,一共有15个引脚,正合适用AVR来驱动做一个钟。
笔段式LCD屏的结构与LED数码管很相似,但是由于是液晶,工作机理上不同,驱动方式也有很大差异:
(1) LED有正负之分,液晶笔划没有。
(2) LED在直流电压下工作,液晶需要交流电压,防止电解效应。
(3) LED需要电流提供发光的能量,液晶笔划显示状态下电流非常微弱。
(4) LED对微小电流不反应,液晶则很敏感。
不难看出,用LED的驱动方式来对待LCD屏是行不通的。我在买回来测试这块屏之前没有意识到,于是走了不少的弯路。与LED驱动不同的是需要给每个笔划加上一个交流电压。一般用30-60Hz的方波就可以了,频率再低显示会有所波动,频率高了功耗也会增加,因为LCD对电路呈现容性。而且,正负电压都可以“点亮”液晶。
好在AVR的I/O口可以三态输出,也就是除了高/低电平,还可以呈现高阻抗,相当于断开连接。于是我想到了这样的办法:不需要显示的那一组笔划对应的公共端悬空(I/O口选择三态),那么就不会加上电压了。照这个思路,我的实验电路焊好,出来的显示却是一团糟:笔划都黑了看不清。我这才考虑到液晶本身的问题:阻抗高,而且有电容,是不可一边悬空的!这个道理也许跟CMOS输入端差不多。查找了一些关于液晶的资料,大致知道LCD屏不是那么简单的,驱动方式通常是1/N, 也就是电压不止高低两档。可是单片机I/O没有那么多输出状态可以选择。
1/2 Bias驱动
不显示的液晶笔划两端电压相等,显示的不等。这样一个要求在扫描方式
下不能满足,于是改为电压等级不同。1/2 Bias驱动就是这样的,如下:
COM1 V+ ---- ----
1/2 ---- ---- ---- ----
GND ---- ----
COM2 V+ ---- ----
1/2 ---- ---- ---- ----
GND ---- ----
SEG1 V+ -------- --------
1/2
GND -------- --------
SEG2 V+ ---- -------- ----
1/2
GND -------- --------
如此,在 COM1,SEG1 选择的笔划上,加上的电压为 -1/2, -1, +1/2, +1 ... 在 COM1,SEG2 选择的笔划上,加上的电压为 +1/2, -1, -1/2, +1 ... 在 COM2,SEG1 选择的笔划上,加上的电压为 -1, -1/2, +1, +1/2 ...在 COM2,SEG2 选择的笔划上,加上的电压为 0, -1/2, 0, +1/2 ...
计算一下大致的平均功率(如果液晶灰度与电压平方成正比,实际不是这样)前三者是一样的,都是 1+(1/2)^2=5/4, 对于最后一个 0+(1/2)^2=1/4 因此显示的功率比为 5:1, 显示状态会是这样:
SEG1 SEG2
: :
COM1 - - - O - - - O
: :
COM2 - - - O - - - x
AVR I/O没有能力输出 1/2 Vcc 的电压(ADC在这里就不要考虑了, 浪费I/O还不如用静态液晶屏), 因此没有办法实现真正的 1/2 Bias驱动。但是注意到要提供一个一半电源电压也不是难事,既然AVR I/O口可以三态,我们用两个电阻分压将端口“拉”到1/2 Vcc就好了,于是,1/2 Bias驱动的做法可以这样:
Vcc
|
[ ]
[ ] 1Meg
[ ]
|
Port pin-----+------------ to LCD COMx
|
[ ]
[ ] 1Meg
[ ]
|
GND
取电阻 1Meg 是综合耗电与分压效果考虑的。这样在 COMx 就可以产生三种电压值,就达到了1/2 Bias动态驱动的目的。实现起来在前面的基础上增加电阻即可,我的屏有4个公共端,因此用了8个电阻,数字就能够显示出来了。
虽然显示的确做到了,然而效果却不能让我满意。具体表现就是需要正对着LCD屏看才是很清晰的;如果斜着看,就可能一片混浊了,没有达到实用。用2节Ni-MH供电时候正着看没问题,用2节干电池(电压提高一点)就不是很清晰了。如前面的分析,那些没有被选择的笔段其实也加上了变化的电压,只不过与选择的比段相比电压平均有效值低一些。这两个的差异足够显著,才能保证显示效果。
再分析 1/2 Bais 驱动在我的LCD屏上 1/4 分时扫描的结果:一个周期内,“点亮”的笔段平均功率=1^2+(1/2)^2+(1/2)^2+(1/2)^2=7/4, 而没有被“点亮”的笔段为=0+(1/2)^2+(1/2)^2+(1/2)^2=3/4, 两者之比 7:3
跟前面的例子分析对比看出,从 1/2 分时扫描变到 1/4 分时扫描,显出来的笔段和不显的笔段上,电压产生平均功率的对比从 5:1 变到 7:3 了。我尝试从软件上改变扫描时序,也不能改进显示效果,看来 1/2 Bias 不够用的了。
我查了Nokia 3310液晶手册其中对于LCD电压输出时序的描述。恰好里面有一个图,绘出了行和列控制线上的波形。从坐标轴上看出Vlcd和Vss之间另外还有4个电压等级。这么多种电压用AVR I/O实现已经不现实了。
我再考虑选用带有LCD驱动功能的MCU, AVR只有一款ATmega169, 封装形式不适合DIY。Microchip有一款PIC16F913, 有28DIP的封装,看上去正合适。暂时不知道价格,我先找来它的手册看看。详细看了LCD驱动模块的部分,我发现PIC16F913也只有1/2 Bias驱动和1/3 Bias驱动两种选项,分时最多为1/4分时驱动,对于我的屏正好。
1/3 Bias 驱动需要将Vcc--GND之间的电压三等分,一个周期驱动波形示例如下:
COM1: V+ --------
2/3 --------
1/3 --------
GND --------
COM2: V+ --------
2/3 --------
1/3 --------
GND --------
SEG1: V+ --------
2/3 --------
1/3 --------
GND --------
SEG2: V+ --------
2/3 --------
1/3 --------
GND --------
在 (COM1,SEG1) 笔段上,电压为 +1, -1/3, -1, +1/3 ... 在(COM1,SEG2)上为 +1/3, +1/3, -1/3, -1/3 ... 在(COM2,SEG1)上:+1/3, +1/3, -1/3, -1/3 ... 在(COM2,SEG2)上:-1/3, +1, +1/3, -1 ...
于是计算平均功率,在 (COM1,SEG1)和(COM2,SEG2)上面是 2*1^2+2*(1/3)^2=20/9 在(COM1,SEG2)和(COM2,SEG1)上面是 4*(1/3)^2=4/9, 两者之比 5:1
假如不是上图的 1/2 分时驱动而是 1/4 分时驱动,这个比例将变为
2*1^2+6*(1/3)^2 vs 8*(1/3)^2 = 3:1
若将原来的 1/2 Bias 改用 1/3 Bias 驱动,对于我的LCD屏这个比值从 7:3 改善为 3:1 了。既然PIC16F913只设计了 1/2 Bias与1/3 Bias,用起来应该问题不大。
AVR单个I/O口要实现4种电压输出——不可能吧,我是想不出来了。AVR最多只有三种电压输出,能不能对这个电压再做等分呢?一番思索之后我想这样行不行:就4等分吧.
COM1: V+ --------
3/4
1/2 -------- --------
1/4
GND --------
COM2: V+ --------
3/4
1/2 -------- --------
1/4
GND --------
SEG1: V+
3/4 -------- ---------
1/2
1/4 ----------------
GND
SEG2: V+
3/4 ----------------
1/2
1/4 -------- ---------
GND
我的做法就是 SEGx 输出有两种:3/4*Vcc 和 1/4*Vcc, 而 COMy 输出有三种:Vcc, GND, 1/2*Vcc. 对于每个I/O口,并不需要4种电压输出。当然这样跟1/3 Bias驱动是不一样的,但是却达到了 1/3 Bias 驱动的效果,只不过加在液晶笔段上的电压 值更大 不是 Vcc 而是 3/4*Vcc 了,因此电源电压也需要提高。这里计算省略。
这种驱动方式我称之为 "伪1/3 Bias驱动". 对于 COMy 的处理和前面一样,对于 SEGx, 将I/O输出电压改变一下,高电平3/4*Vcc, 低电平1/4*Vcc就好了。我的做法是:
/-------------- I/O Port pin
|
[ ]
[ ] 1Meg
[ ]
|
to LCD SEGx --------------+
|
[ ]
[ ] 1Meg
[ ]
|
|
1/2 Vcc
这里的 1/2 Vcc 可以将电源电压用电阻分压得到,我想的办法是直接接个几uF电容到GND, 实验是成功的。因为随着扫描的进行,这个地方的平均电压是输出高电平和低电平的一半。
目前我做了一个Mega48V的秒计数器,再改改就能把钟做出来了。
这是我的程序:(因为刚刚开始用AVR,从最简单的开始,就直接用汇编了)
Timer2用外接32768晶振提供时钟,整个系统耗电大约30微安。
; lcddisplay.asm
; Test raw LCD display
.include "m48def.inc"
.org 0x0000
rjmp start
.org OC2Aaddr
rjmp isr_timer2
.org 0x0020
table:
.DB 0b11101101, 0b00101000, 0b10110101, 0b10111001
.DB 0b01111000, 0b11011001, 0b11011101, 0b10101000
.DB 0b11111101, 0b11111001
start:
ldi r16, 1< out MCUCR, r16 ; disable all I/O pull-up
ldi r16, 1< sts ASSR, r16 ; enable asynchronous mode
ldi r16, 1< sts TCCR2A, r16 ; CTC mode
ldi r16, 31
sts OCR2A, r16 ; preset compare A
ldi r16,1< ; ldi r16,1< sts TCCR2B, r16
ldi r16, 1< out TIFR2, r16 ; clear flag
ldi r16, 1< sts TIMSK2, r16 ; enable interrupt on compare match A
ser r16
out DDRD, r16 ; Port D output -- LCD segment control
clr r5
ldi r16, 0x55
mov r6, r16
clr r7
clr r8
ldi r16, 9
mov r10, r16
mov r11, r16
mov r12, r16
mov r13, r16
dec r10
sei ; enable global interrupt
ldi r16, (1< out SMCR, r16 ; use Idle mode here, waiting 1 second
clr r2
iniw:sleep
dec r2
brne iniw
ldi r16, (1< out SMCR, r16 ; use power-save mode
nop
nop
clr r2
loop:
nop
nop
sleep
nop
nop
dec r2
dec r2
breq adjtime
rjmp loop
adjtime:
ldi r17, 10
inc r10
cp r10, r17
brne updcount
clr r10
inc r11
cp r11, r17
brne updcount
clr r11
inc r12
cp r12, r17
brne updcount
clr r12
inc r13
cp r13, r17
brne updcount
clr r13
updcount:
rcall calcor
rjmp loop
isr_timer2:
clr r16
out DDRC, r16 ; float all COMx pins
bst r4, 1
brts show34
bst r4, 0
brts show2
mov r0, r5
ldi r18, 1
rjmp sel
show2:
mov r0, r6
ldi r18, 1<<1
rjmp sel
show34:
bst r4, 0
brts show4
mov r0, r7
ldi r18, 1<<2
rjmp sel
show4:
mov r0, r8
ldi r18, 1<<3
sel:
bst r4, 2
brtc lcden
com r0
com r16
lcden:
out PORTC, r16
out PORTD, r0
out DDRC, r18
iext:inc r4
reti
calcor: ; translate R10~~R13 to R5~~R8
clr r5
clr r6
clr r7
clr r8
ldi ZH, high(table<<1)
ldi ZL, low(table<<1)
add ZL, r10
lpm ; load table data to R0
rcall filler
ldi ZL, low(table<<1)
add ZL, r11
lpm
rcall filler
ldi ZL, low(table<<1)
add ZL, r12
lpm
rcall filler
ldi ZL, low(table<<1)
add ZL, r13
lpm
rcall filler
ret
filler:
rol r0
rol r5
rol r0
rol r5
rol r0
rol r6
rol r0
rol r6
rol r0
rol r7
rol r0
rol r7
rol r0
rol r8
rol r0
rol r8
ret
补充一下, 液晶屏的引脚与笔划的对应跟LED数码管可能不一样, 我这个屏是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
< 1f 1a 2f 2a 3f 3a 4f 4a > -- ---- COM1
< 1g 1b 2g 2b 2g 3b 4g 4b > -- ---- COM2
< 1e 1c 2e 2c : 3e 3c 4e 4c > -- ---- COM3
1d 1h 2d 2h 3d 3h 4d -- ---- COM4
其中 12,13,14,15 是4个公共端; 2,3控制 个数字; 4,5第二个; 7,8第三个; 9,10第四个。
############################################################
以前有一个家用热水器控制项目,硬件与软件是分开搞的.为了省成本,硬件工程师不用专用液晶驱动IC,把我搞得够呛.整个项目20天时间,光液晶驱动程序用去了10多天.
I/O端口SEG与COM口分别串接一只电阻,再并联一只电阻到地,
这样,对液晶来讲,I/O口是电源/地,定时改变SEG/COM的H/L电平,相当于1/2VCC的交流信号.
当SEG/COM同时为H或L电平时,对液晶来讲,都是关,SEG/COM电平不等时,段码就能显示出来.
明白了这个道理,用C语言写出来不难.