Michael Cartwright
Published

PS/2 Keyboard and LCD interface for 6502

Improved version of PS/2 keyboard and 20x4 LCD interface and drivers for the Ben Eater 6502 computer.

IntermediateFull instructions provided8 hours619
PS/2 Keyboard and LCD interface for 6502

Things used in this project

Hardware components

PS/2 Keyboard
×1
RGB Backlight LCD - 16x2
Adafruit RGB Backlight LCD - 16x2
×1

Software apps and online services

KiCad
KiCad
VASM - 6502 assembler

Hand tools and fabrication machines

SIGLENT SDS1104X-E Oscilloscope

Story

Read more

Schematics

Keyboard and LCD hardware interface for 6502 computer

To be used in conjunction with the regular Ben Eater 6502 design.
Where my version differs:
- 2x 10K pullups added on keyboard lines (R1 and R2)
- 10uF capacitor added on keyboard power leads (C1)
- R3 value calculated using oscilloscope (5.6K in my case)
- shaping/cleaning of KB_CLK (U2C and U2D)
- shaping/cleaning of KB_DATA (U2F and U2E)
Liberal use of 0.1uF bypass capacitors is not shown.

Code

Reworked LCD and Keyboard drivers

Assembly x86
This was derived from Ben Eater's work but also includes my own work to make the 4 line LCD work correctly and improvements to the keyboard interface (do as little as possible within the interrupt service routine).
PORTB = $6000
PORTA = $6001
DDRB = $6002
DDRA = $6003
PCR = $600C
IFR = $600D
IER = $600E

E  = %01000000
RW = %00100000
RS = %00010000

CPUFREQUENCY = 1 ; clock frequency in MHz for delay subroutine

; zero page variables:
kb_wptr  = $0000
kb_rptr  = $0001
kb_flags = $0002

RELEASING    = %00000001
LEFTSHIFT    = %00000010
RIGHTSHIFT   = %00000100
LEFTCONTROL  = %00001000

lcd_col  = $0003 ; current LCD col
lcd_row  = $0004 ; current LCD row
str_ptr  = $0005 ; 2 byte string pointer

kb_buffer = $0200  ; 256-byte kb buffer 0200-02ff

; 20x4
LCDROWS = 4
LCDCOLS = 20

; 16x2
;LCDROWS = 2
;LCDCOLS = 16

  .org $8000

reset:
  ldx #$ff
  txs

  lda #$01
  sta PCR        ; set positive active edge to trigger for interrupt on transition to high for CA1 ($0 would trigger on transition to low)
  lda #$82
  sta IER        ; set CA1 bit in IER to enable CA1 as an interrupt pin
  cli

  lda #%11111111 ; Set all pins on port B to output
  sta DDRB
  lda #%00000000 ; Set all pins on port A to input
  sta DDRA

  jsr lcd_init   ; Note: along with delay, this bludgeons A, X and Y (cold and warm reset of LCD)
  
  lda #%00101000 ; Set 4-bit mode; 2-line display; 5x8 font
  jsr lcd_instruction
  lda #%00001110 ; Display on; cursor on; blink off
  jsr lcd_instruction
  lda #%00000110 ; Increment and shift cursor; don't shift display
  jsr lcd_instruction
  lda #%00000001 ; Clear screen
  jsr lcd_instruction
  
  lda #$00
  sta kb_wptr
  sta kb_rptr
  sta kb_flags
  sta lcd_col
  sta lcd_row
  jsr lcd_setcursor
  
  lda #(prompt & $00F0)
  sta str_ptr
  lda #(prompt >> 8)
  sta str_ptr + 1
  jsr print_str

loop:
  sei              ; disable interrupts
  lda kb_rptr
  cmp kb_wptr
  cli              ; enable interrupts
  bne process_code ; we have a code in the buffer
  bra loop

process_code:
  ldx kb_rptr
  lda kb_buffer, x
  inc kb_rptr
  
  ; debugging only
  ; jsr print_hex
  ;pha
  ;lda #" "
  ;jsr print_char
  ;lda #" "
  ;jsr print_char
  ;pla
  ; end of debugging only
  
  cmp #$F0 ; release code?
  bne notreleasenext
  lda kb_flags
  ora #RELEASING
  sta kb_flags
  jmp loopexit
  
 notreleasenext:
  ; not release code
  pha
  lda kb_flags
  and #RELEASING
  bne releasecode
  pla
  
  ; regular code, not release code
  cmp #$14 ; left control?
  bne notleftcontrol
  lda kb_flags
  ora #LEFTCONTROL
  sta kb_flags
  jmp loopexit
notleftcontrol
  cmp #$12 ; left shift?
  bne notshiftleft
  lda kb_flags
  ora #LEFTSHIFT
  sta kb_flags
  jmp loopexit
notshiftleft:
  cmp #$59 ; right shift?
  bne notshiftright
  lda kb_flags
  ora #RIGHTSHIFT
  sta kb_flags
  jmp loopexit
notshiftright:
  
  ; decode code here and print
  tax
  lda kb_flags
  and #(LEFTSHIFT | RIGHTSHIFT)
  beq notshifted
  lda keymap_shifted, x   ; map to shifted character code
  bra testunknown
notshifted:
  lda kb_flags
  and #LEFTCONTROL
  beq regular
  lda keymap_controlled, x   ; map to controlled character code
  bra testunknown
regular:
  lda keymap, x           ; map to character code
testunknown:

  cmp #$1B; escape?
  bne notescape
  jsr lcd_clear
  jmp loopexit
notescape:
  cmp #$08; backspace?
  bne notbackspace
  jsr lcd_backspace
  lda #" "
  jsr print_char
  jsr lcd_backspace
  jmp loopexit
notbackspace:
  cmp #$0A; enter?
  bne notenter
  jsr lcd_enter
  bra loopexit
notenter:

  cmp #"?"
  bne notunknown
  cpx #$4A                ; question key
  beq notunknown
  jsr print_hex
  bra loopexit
  
notunknown:
  jsr print_char
  bra loopexit
  
releasecode: 
  lda kb_flags
  eor #RELEASING
  sta kb_flags
  pla

  cmp #$14 ; left control for RC2014 keyboard
  bne notleftcontrol2
  lda kb_flags
  eor #LEFTCONTROL
  sta kb_flags
  bra loopexit
notleftcontrol2:
  cmp #$12 ; left shift
  bne notshiftleft2
  lda kb_flags
  eor #LEFTSHIFT
  sta kb_flags
  bra loopexit
notshiftleft2:
  cmp #$59 ; right shift
  bne notshiftright2
  lda kb_flags
  eor #RIGHTSHIFT
  sta kb_flags
  bra loopexit
notshiftright2:  

  ; ignore released code
  bra loopexit
  
loopexit:
  jmp loop
    
lcd_wait:
  pha
  lda #%11110000  ; LCD data is input
  sta DDRB
lcdbusy:
  lda #RW
  sta PORTB
  lda #(RW | E)
  sta PORTB
  lda PORTB       ; Read high nibble
  pha             ; and put on stack since it has the busy flag
  lda #RW
  sta PORTB
  lda #(RW | E)
  sta PORTB
  lda PORTB       ; Read low nibble
  pla             ; Get high nibble off stack
  and #%00001000
  bne lcdbusy

  lda #RW
  sta PORTB
  lda #%11111111  ; LCD data is output
  sta DDRB
  pla
  rts
  
delay:
  ldx #CPUFREQUENCY
cpuloop:
  ; http://forum.6502.org/viewtopic.php?p=62581#p62581
  ; delay 9*(256*A+Y)+8 cycles
  ; assumes that the BCS does not cross a page boundary
  ; A and Y are the high and low bytes (respectively) of a 16-bit value; multiply that 16-bit value by 9, then add 8 and you get the cycle count
  ;
  pha
  phy
  
delayloop:
  cpy #1        ; 2
  dey           ; 2
  sbc #0        ; 2
  bcs delayloop ; 3
  
  ply
  pla
  
  dex
  bne cpuloop
  rts

lcd_init:
  ; as per Figure 24 (page 46) of the Hitachi data sheet - yes, much like beating it with a rock!
  
  ; delay 50000 us ; > 40ms for Vcc to rise above 2.7V
  lda #21
  ldy #128
  jsr delay
  
  lda #%00000011 ; Set 4-bit mode
  sta PORTB
  ora #E
  sta PORTB
  and #%00001111
  sta PORTB
  
  ; delay 4500 us
  lda #1
  ldy #243
  jsr delay
  
  lda #%00000011 ; Set 4-bit mode
  sta PORTB
  ora #E
  sta PORTB
  and #%00001111
  sta PORTB
  
  ; delay 150 us
  lda #0
  ldy #16
  jsr delay
    
  lda #%00000011 ; Set 4-bit mode
  sta PORTB
  ora #E
  sta PORTB
  and #%00001111
  sta PORTB
  
  ; This 4 bit initialization works well for cold reset (no power to the LCD) but not for resetting
  ; an already initialized and powered up LCD (without power cycling).
  ; More luck with warm reset with even number of 4 bit writes (in case LCD is already in 4 bit mode)
  
  lda #%00000010 ; Set 4-bit mode
  sta PORTB
  ora #E
  sta PORTB
  and #%00001111
  sta PORTB
  
  rts

lcd_instruction:
  jsr lcd_wait
  pha
  lsr
  lsr
  lsr
  lsr            ; Send high 4 bits
  sta PORTB
  ora #E         ; Set E bit to send instruction
  sta PORTB
  eor #E         ; Clear E bit
  sta PORTB
  pla
  and #%00001111 ; Send low 4 bits
  sta PORTB
  ora #E         ; Set E bit to send instruction
  sta PORTB
  eor #E         ; Clear E bit
  sta PORTB
  rts
  
lcd_writedata:
  pha
  lsr
  lsr
  lsr
  lsr             ; Send high 4 bits
  ora #RS         ; Set RS
  sta PORTB
  ora #E          ; Set E bit to send instruction
  sta PORTB
  eor #E          ; Clear E bit
  sta PORTB
  pla
  and #%00001111  ; Send low 4 bits
  ora #RS         ; Set RS
  sta PORTB
  ora #E          ; Set E bit to send instruction
  sta PORTB
  eor #E          ; Clear E bit
  sta PORTB
  rts

lcd_clear:
  pha
  lda #$00
  sta lcd_col
  sta lcd_row
  lda #%00000001 ; Clear screen
  ;jsr lcd_instruction
  ;jsr lcd_setcursor
  pla
  rts
  
lcd_setcursor: ; (lcd_col, lcd_row)
  pha
  phx
  ldx lcd_row
  cpx #LCDROWS
  beq lcdskipsetcursor ; don't wrap around if (col,row) out of range (less confusion)
  
  lda lcdrowstart, x
  adc lcd_col
  ora #%10000000 ; Set DDRAM address
  jsr lcd_instruction
  
lcdskipsetcursor:
  plx
  pla
  rts
  
lcd_backspace:
  pha
  phx
  
  lda lcd_col
  beq colzero
  dec
  sta lcd_col
  bra backspaceexit
  
colzero:
  ldx lcd_row
  beq backspaceexit
  dex
  stx lcd_row
  lda #LCDCOLS
  dec
  sta lcd_col
  
backspaceexit:
  plx
  pla
  jsr lcd_setcursor
  rts
  
lcd_enter:
  phx
  
  ldx lcd_row
  inx
  cpx #LCDROWS
  beq lcdskipenter ; don't enter on last row
  stx lcd_row
  ldx #$00
  stx lcd_col
  
lcdskipenter:
  plx
  jsr lcd_setcursor
  rts
  
print_hex
  phx
  pha
  
  pha
  lsr
  lsr
  lsr
  lsr
  tax
  lda hexmap, x
  jsr print_char
  pla

  and #$0F
  tax
  lda hexmap, x
  jsr print_char
  
  pla
  plx
  rts
  
print_str:
  phy
  pha
  ldy #0

print_next:
  lda (str_ptr), y
  beq print_exit
  jsr print_char
  iny
  bra print_next
print_exit:
  pla
  ply
  rts

print_char:
  pha
  phx
  ldx lcd_row
  cpx #LCDROWS
  beq exit_print_char ; don't wrap around if (col,row) out of range (less confusion)
  
  jsr lcd_setcursor
  jsr lcd_wait
  jsr lcd_writedata
  
  ; move cursor to next cell
  inc lcd_col
  lda #LCDCOLS
  cmp lcd_col
  bne exit_print_char
  lda #0
  sta lcd_col
  inc lcd_row
  
  jsr lcd_setcursor ; to display next cell position
  
exit_print_char:
  plx
  pla
  rts


; IRQ vector points here
keyboard_interrupt:
  pha
  phx

  lda PORTA         ; read key code (reading PORTA also clears the interrupt)
  ldx kb_wptr       ; push it into key code buffer
  sta kb_buffer, x
  inc kb_wptr

  plx
  pla
  rti

nmi:
  rti

  .org $fc00
keymap:
  .byte "????????????? `?" ; 00-0F
  .byte "?????q1???zsaw2?" ; 10-1F
  .byte "?cxde43?? vftr5?" ; 20-2F
  .byte "?nbhgy6???mju78?" ; 30-3F
  .byte "?,kio09??./l;p-?" ; 40-4F
  .byte "??'?[=????",$0A,"]?\??" ; 50-5F
  .byte "??????",$08,"??1?47???" ; 60-6F
  .byte "0.2568",$1b,"??+3-*9??" ; 70-7F
  .byte "????????????????" ; 80-8F
  .byte "????????????????" ; 90-9F
  .byte "????????????????" ; A0-AF
  .byte "????????????????" ; B0-BF
  .byte "????????????????" ; C0-CF
  .byte "????????????????" ; D0-DF
  .byte "????????????????" ; E0-EF
  .byte "????????????????" ; F0-FF
keymap_shifted:
  .byte "????????????? ~?" ; 00-0F
  .byte "?????Q!???ZSAW@?" ; 10-1F
  .byte "?CXDE$#?? VFTR%?" ; 20-2F
  .byte "?NBHGY^???MJU&*?" ; 30-3F
  .byte "?<KIO)(??>?L:P_?" ; 40-4F
  .byte '??"?{+????",$0A,"}?|??' ; 50-5F
  .byte "?????????1?47???" ; 60-6F
  .byte "0.2568???+3-*9??" ; 70-7F
  .byte "????????????????" ; 80-8F
  .byte "????????????????" ; 90-9F
  .byte "????????????????" ; A0-AF
  .byte "????????????????" ; B0-BF
  .byte "????????????????" ; C0-CF
  .byte "????????????????" ; D0-DF
  .byte "????????????????" ; E0-EF
  .byte "????????????????" ; F0-FF
keymap_controlled:
  .byte "????????????? `?" ; 00-0F
  .byte "?????q{???zsa~|?" ; 10-1F
  .byte "?cxde[}??",$1B,"vf_r]?" ; 20-2F
  .byte "?,b",$08,"gy<???.ju=>?" ; 30-3F
  .byte "?,kio-+??./l;p-?" ; 40-4F
  .byte "??'?[=?????]?\??" ; 50-5F
  .byte "?????????1?47???" ; 60-6F
  .byte "0.2568???+3-*9??" ; 70-7F
  .byte "????????????????" ; 80-8F
  .byte "????????????????" ; 90-9F
  .byte "????????????????" ; A0-AF
  .byte "????????????????" ; B0-BF
  .byte "????????????????" ; C0-CF
  .byte "????????????????" ; D0-DF
  .byte "????????????????" ; E0-EF
  .byte "????????????????" ; F0-FF

  .org $ffd0
prompt:
  .byte "PS/2: "
  .byte 0

  .org $ffe0
hexmap:
  .byte "0123456789ABCDEF"

  .org $fff0
lcdrowstart:
  .byte $00 ; 20x4 and 16x2
  .byte $40 ; 20x4 and 16x2
  .byte $14 ; 20x4
  .byte $54 ; 20x4
  

; Reset/IRQ vectors
  .org $fffa
  .word nmi
  .word reset
  .word keyboard_interrupt

Credits

Michael Cartwright

Michael Cartwright

21 projects • 13 followers

Comments