使用8086制作LCD显示电压表

虽然很早就接触过单片机并写了不少程序,但是对于汇编语言还是有些发怵。正巧刚刚学完了微机原理课程,于是就想趁着刚学完8086汇编的热乎劲儿,趁热打铁做点东西。经过思考,我决定用8086驱动常见的1602液晶,用外置ADC制作一个电压表。

鉴于8086年代过于久远,找到实际的芯片的难度太高,于是仿真就成了最好的方法。好在,现在对于这种古老的芯片的仿真技术已经非常完善。Proteus是一款老牌仿真软件,可以很好的仿真8086,配合MASM32作为汇编工具,便可以开始进行8086软硬件的设计了。完整的工程(仿真图、源程序)可以在文末找到。

本设计具有如下功能:

  • 读取外部电压并显示在LCD上
  • 设置电压上下限并显示在LCD上
  • 可以通过按键调节电压的上下限
  • 电压超过上下限时蜂鸣器鸣响报警
  • 设置电压上下限时,无法超过输入电压范围(0-5V),并且最高电圧不能低于最低电压。

程序流程图如下。

本设计最重要的部分就是8086处理器及其外围电路了。8086最小系统与接口译码部分的原理图如下所示。

https://img.yuanze.wang/posts/8086-volt-meter/sch1.png
8086最小系统与接口译码部分

接下来是报警与按键部分电路。

https://img.yuanze.wang/posts/8086-volt-meter/sch2.png
报警与按键部分

液晶显示器部分选择了之前51时代常用的1602液晶。这一款液晶使用8080并口时序,使用8086的IO地址译码电路,即可将其作为一个内存访问,大大简化了程序的设计。这也解开了我刚接触1602液晶时对于它这种奇怪的驱动模式的疑惑:都是时代的产物。液晶显示器与接口电路如下所示。

https://img.yuanze.wang/posts/8086-volt-meter/sch3.png
液晶显示器与接口

ADC部分选择了一块年代久远的芯片ADC0805。选择这块芯片的原因有二:一是因为Multisim软件支持对这款芯片进行仿真,二是因为它也具有与1602类似的IO结构,特别适合用8086这种只支持古老的并口时序的处理器驱动。ADC、蜂鸣器接口电路与输入接口电路如下所示。

https://img.yuanze.wang/posts/8086-volt-meter/sch4.png
ADC、蜂鸣器接口电路与输入接口

在仿真程序的过程中,我发现LCD1602在一开始总是无法正常显示,即便我通过示波器确定通信是没有问题的。我第一个想到的就是LCD1602的速度问题。众所周知,51单片机驱动1602都需要加很多的延时才能保证正常显示。本来我以为对于仿真软件来说,这些延时并没有什么必要,但是在加上延时之后,1602突然就可以正常使用了。不得不令人赞叹,仿真软件居然可以仿真到如此细节。

接下来就是完成程序编写了。程序运行后,首先显示作品名称与制作人信息。数秒过后,进入主界面,如下图所示。LCD的第一行显示电压,第二行显示电压报警上下限。此时调整电位器的位置,即可改变ADC的输入电压,并可以在屏幕上看到电压的变化。

https://img.yuanze.wang/posts/8086-volt-meter/lcd.png
主界面

原理图中的四个按键可以分别调整电压报警值的上限与下限。四个按键从上到下的功能分别为电压上限加、电压上限减、电压下限加、电压下限减,每按一次相应的电压变化0.1V。按下以后新的电压值将即时反映在屏幕上。当电位器中点的电压超出所设定的范围后,蜂鸣器响。电压恢复到范围以内之后,蜂鸣器关闭。

 Multisim仿真图与源程序

.MODEL   SMALL
.8086
.data
bootstr1 db 81h,"Voltage Meter",'$'
bootstr2 db 0c1h,"WYZ 1605010419",'$'
line1 db 080h,"  Voltage:    V ",'$'
line2 db 0c0h," H:    V L:    V",'$'
highvol dw 400
lowvol dw 100
currvol dw 0
.code
.startup
start:   cli
  call lcdinit
  ;显示制作者信息
  mov bx,offset bootstr1
  call lcdstr
  mov bx,offset bootstr2
  call lcdstr
  ;延时
  call dly1s
  ;清屏      
  mov bx,offset line1
  call lcdstr
  mov bx,offset line2
  call lcdstr
  
while1:   mov al,8ah;显示地址
  call lcdcmd
  
  call getvol
  call datproc
  mov bx,offset currvol;保存电压值
  mov [bx],ax
  call strconv
  
  mov al,0c3h
  call lcdcmd
  mov bx,offset highvol
  mov ax,[bx]
  call strconv
  
  mov al,0cbh
  call lcdcmd
  mov bx,offset lowvol
  mov ax,[bx]
  call strconv
  
  call keyproc
  call almproc
  
  jmp while1
delayms   proc near
  push cx
  mov cx,25000
dlylop:loop dlylop
  pop cx
  ret
delayms   endp
keyproc   proc near
  push dx
  push ax
  push bx
  push cx
  mov dx,298h
  in al,dx
  
  mov bx,offset highvol
  mov dx,[bx];highvol
  mov bx,offset lowvol
  mov cx,[bx];lowvol
  
  test al,00000010b;检测H+是否按下
  jz next1
  ;H+代码
  cmp dx,500
  jae next1
  add dx,10
  mov bx,offset highvol
  mov [bx],dx
  
next1:   test al,00000100b;H-
  jz next2
  ;H-代码
  cmp dx,cx
  jbe next2
  sub dx,10
  mov bx,offset highvol
  mov [bx],dx
  
next2:   test al,00001000b;L+
  jz next3
  ;L+代码
  cmp cx,dx
  jae next3
  add cx,10
  mov bx,offset lowvol
  mov [bx],cx
  
next3:   test al,00010000b;L-
  jz next4
  ;L-代码
  cmp cx,0
  jbe next4
  sub cx,10
  mov bx,offset lowvol
  mov [bx],cx
  
next4:   test al,00011110b
  jz endl
  call delayms
  
endl:   pop cx
  pop bx
  pop ax
  pop dx
  ret
keyproc   endp
;蜂鸣器报警处理
almproc   proc near
  push bx
  push ax
  push cx
  push dx
  
  mov dx,2a0h
  
  mov bx,offset currvol
  mov ax,[bx]
  mov bx,offset highvol
  mov cx,[bx]
  
  cmp ax,cx
  jae buzzon
  
  mov bx,offset lowvol
  mov cx,[bx]
  cmp ax,cx
  jbe   buzzon
  
  jmp buzzoff
  
buzzon:   mov al,1
  out dx,al
  jmp exit
  
buzzoff:mov al,0
  out dx,al
  
exit:   pop dx
  pop cx
  pop ax
  pop bx
  ret
almproc   endp
;延时一秒
dly1s   proc near
  push bx
  push cx
  mov bx,200
LP1:   mov cx,1000
LP2:   loop LP2
  dec bx
  jnz LP1
  pop cx
  pop bx
  ret
dly1s   endp
;将AL的AD值处理为电压值
datproc   proc near
  push cx
  push dx
  
  mov ah,0
  mov cx,200
  mul cx;AL乘以CX 结果在AX
  mov cx,102
  mov dx,0
  div cx;DX.AX/CX 商在AX 余数在DX
  
  pop dx
  pop cx
  ret
datproc endp
  
;将AX的数值显示到屏幕上
strconv   proc near
  push cx
  push dx
  push ax
  
  ;百位
  mov cl,100
  div cl;AX除以CL 商在AL中 余数在AH中
  add al,'0';转换成ASCII
  call lcddat
  inc bx
  ;小数点
  mov al,'.'
  call lcddat
  inc bx
  ;十位
  mov al,ah
  mov ah,0;将余数挪到低位 清空高位
  mov cl,10
  div cl;AX除以CL 商在AL中 余数在AH中
  mov dl,ah
  add al,'0'
  call lcddat
  inc bx
  ;个位
  mov al,dl
  add al,'0'
  call lcddat
  
  pop ax
  pop dx
  pop cx
  ret
strconv   endp
  
;查询等待ADC转换完成
chkbusy   proc near
  push dx
  push ax
rechk:   mov dx,298h
  in al,dx
  test al,1
  jnz rechk
  pop ax
  pop dx
  ret
chkbusy endp
;读取AD值到AL
getvol   proc near
  push dx
  mov dx,290h
  mov al,0;数据随意
  out dx,al;发送一个写信号 启动转换
  call chkbusy;等待转换完成
  in al,dx;读取AD值到AL
  pop dx
  ret
getvol   endp      
;显示BX所指的内存单元中的一段字符串 以$结尾 第一个字节为显示位置
lcdstr   proc near
  mov al,[bx]
  call lcdcmd;设置显示位置
nextchr:inc bx
  mov al,[bx]
  cmp al,'$'
  je gotoret
  call lcddat
  jmp nextchr
gotoret:ret
lcdstr   endp
;将AL的内容写进数据寄存器
lcddat   proc near
  push dx
  mov dx,280h
  out dx,al
  
  call delay
  
  mov dx,288h
  mov al,00000001b
  out dx,al;EN=0 RW=0 RS=1
  
  call delay
  
  mov al,00000101b
  out dx,al;EN=1 RW=0 RS=1
  
  call delay
  
  mov al,00000001b
  out dx,al;EN=0 RW=0 RS=1
  pop dx
  ret
lcddat endp
;将AL的内容写进指令寄存器
lcdcmd   proc near
  push dx
  mov dx,280h
  out dx,al
  
  call delay
  
  mov dx,288h
  mov al,00000000b
  out dx,al;EN=0 RW=0 RS=0
  
  call delay
  
  mov al,00000100b
  out dx,al;EN=1 RW=0 RS=0
  
  call delay
  
  mov al,00000000b
  out dx,al;EN=0 RW=0 RS=0
  
  pop dx
  ret
lcdcmd endp
lcdinit   proc near
  mov al,38h;8线模式 显示2行 5*7点阵
  call lcdcmd
  mov al,03h;光标归位
  call lcdcmd
  mov al,06h;光标右移 不移动屏幕
  call lcdcmd
  mov al,0ch;显示开 无光标 不闪烁
  call lcdcmd
  mov al,01h;清屏
  call lcdcmd
  
  mov al,80h
  call lcdcmd
  ret
lcdinit endp
delay   proc near
  push cx
  mov cx,50
 
dlyloop:loop dlyloop
  pop cx
  ret
delay   endp
.data
.stack
END