In December 1982, Santa delivered a Commodore 64, which became an obsession for me during my childhood and teen years. I quickly discovered that most of the graphic operations I wanted to perform were not possible using the BASIC interpreter built into the OS. Fast, smooth animations were only possible using assembly language routines either natively run or called from BASIC programs. I especially liked using Sprites, which were called Movable Object Blocks (MOB), a feature of the Commodore 64’s VIC-II chip. Sprite animations were slow in BASIC, and I wanted to animate them at much faster speeds. The only way to accomplish this was using assembly language, since BASIC must interpret all of its commands before translating them into machine code, which made the code run very slow. Assembly language is the closest to machine code that is human readable, and higher level languages (like C or C++) which compile code into machine code were not very practical at the time. I first learned assembly language by reading magazines like Compute!’s Gazatte and disassembling commercial games. Unfortunately, disassemblers do not restore mnemonics, causing the code to be obfuscated.
At the heart of the C64 is the 6510 microprocessor, which is a modified version of the extremely popular 6502 processor used in the Apple II, and many other machines in the 1980s. The 6502 processor is still available in a CMOS version from many suppliers, so I decided to build a simple breadboard computer based on Ben Eater’s design. In addition to the LCD, I wanted to implement a simple Finite-state Machine in Assembly language which uses the I/O buttons to change state.
In order to watch the operation at slow speeds, I added a 1 MHz oscillator with clock divider circuit. This is a series of cascaded 74HC193 Synchronous 4-Bit Up/Down Counters. I also wanted to single step through CPU clock cycles, so I created a debounced switch circuit for clock pulses.
I also used my Saleae Logic Pro 16 to analyze the data bus signals for the LCD commands.
In order to implement the finite-state machine, I modified the assembly code that Ben Eater developed in his video series. I added the finite-state code, implemented an indexed-indirect lookup table and null-terminated string printing routine using indirect-indexed looping.
PORTB = $6000
PORTA = $6001
DDRB = $6002
DDRA = $6003
E = %10000000
RW = %01000000
RS = %00100000
; some memory for the 16 bit addition
num1_low = $200
num1_high = $201
num2_low = $202
num2_high = $203
result_low = $204
result_high = $205
state_machine = $206
; zero page pointers
zp_msg_ptr_lb = $00
zp_msg_ptr_ub = $01
; the location of the strings
msg_loc_lb = $00
msg_loc_ub = $f0
msg_loc = $f000
; the number of message strings x2 (for the lower byte and upper byte)
num_msgs_x2 = $0c
.org $8000
reset:
; initialize the stack pointer
ldx #$ff
txs
; initialize the x and y registers
ldx #$00
ldy #$00
; initialize the state machine
lda #$ff
sta state_machine
; initialize the zero page pointer
lda #msg_loc_lb
sta zp_msg_ptr_lb
lda #msg_loc_ub
sta zp_msg_ptr_ub
; This is where I set up the indirect addressing for the strings
; in two loops...we are looking for the start and end of each
; string and entering the upper and lower bytes of the address
; into zero page space
outer_loop:
cpx #num_msgs_x2
beq finished_zp_init
lda #msg_loc_lb
sta num1_low
lda #msg_loc_ub
sta num1_high
sty num2_low
lda #$00
sta num2_high
jsr add16
lda result_low
sta zp_msg_ptr_lb,x
lda result_high
sta zp_msg_ptr_ub,x
set_msg_ptrs:
lda msg_loc,y
cmp #$00
beq done_msg
iny
jmp set_msg_ptrs
done_msg:
iny
inx
inx
jmp outer_loop
finished_zp_init:
ldx #$ff
ldy #$00
; This is where we are setting up the LCD
lda #%11111111 ; Set all pins on port B to output
sta DDRB
lda #%11100000 ; Set top 3 pins on port A to output
sta DDRA
lda #%00111000 ; Set 8-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 display
jsr lcd_instruction
; this is the main loop where we are checking for button input
; and running the state machine
loop:
jsr check_input
jsr check_buttons
jmp loop
; printing a string using zero page indirect indexed addressing
print_string:
cpx #$00
beq print_msg_init
cpx #$02
beq print_msg_btn0
cpx #$04
beq print_msg_btn1
cpx #$06
beq print_msg_btn2
cpx #$08
beq print_msg_btn3
cpx #$0a
beq print_msg_btn4
print_msg_init:
lda ($00),y
jmp done_selection
print_msg_btn0:
lda ($02),y
jmp done_selection
print_msg_btn1:
lda ($04),y
jmp done_selection
print_msg_btn2:
lda ($06),y
jmp done_selection
print_msg_btn3:
lda ($08),y
jmp done_selection
print_msg_btn4:
lda ($0a),y
done_selection:
beq done_printing
jsr print_char
iny
jmp print_string
done_printing:
ldy #$00
rts
lcd_wait:
pha
lda #%00000000 ; Port B is input
sta DDRB
lcdbusy:
lda #RW
sta PORTA
lda #(RW | E)
sta PORTA
lda PORTB
and #%10000000
bne lcdbusy
lda #RW
sta PORTA
lda #%11111111 ; Port B is output
sta DDRB
pla
rts
lcd_instruction:
jsr lcd_wait
sta PORTB
lda #0 ; Clear RS/RW/E bits
sta PORTA
lda #E ; Set E bit to send instruction
sta PORTA
lda #0 ; Clear RS/RW/E bits
sta PORTA
rts
print_char:
jsr lcd_wait
sta PORTB
lda #RS ; Set RS; Clear RW/E bits
sta PORTA
lda #(RS | E) ; Set E bit to send instruction
sta PORTA
lda #RS ; Clear E bits
sta PORTA
rts
check_input:
lda #0 ; Clear RS/RW/E bits on LCD
sta PORTA
lda PORTA ; Read the data on port A
and #%00011111 ; Mask the lower 5 bits
rts
check_buttons:
; since we are using the accumulator from the last call
; we need to push it to the stack in order to restore
; it after we change the value
pha
lda state_machine
cmp #$ff
pla
beq initial_state
cmp #%00011110
beq button_0
cmp #%00011101
beq button_1
cmp #%00011011
beq button_2
cmp #%00010111
beq button_3
cmp #%00001111
beq button_4
initial_state:
; initialize the state machine
lda state_machine
cmp #$ff
bne completed_no_button
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$00
lda #$00
sta state_machine
jsr print_string
completed_no_button:
jmp finished_btns
button_0:
; we only want to execute the block if the state changed
lda state_machine
cmp #$01
beq completed_button_0
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$02
lda #$01
sta state_machine
jsr print_string
completed_button_0:
jmp finished_btns
button_1:
; we only want to execute the block if the state changed
lda state_machine
cmp #$02
beq completed_button_1
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$04
lda #$02
sta state_machine
jsr print_string
completed_button_1:
jmp finished_btns
button_2:
; we only want to execute the block if the state changed
lda state_machine
cmp #$03
beq completed_button_2
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$06
lda #$03
sta state_machine
jsr print_string
completed_button_2:
jmp finished_btns
button_3:
; we only want to execute the block if the state changed
lda state_machine
cmp #$04
beq completed_button_3
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$08
lda #$04
sta state_machine
jsr print_string
completed_button_3:
jmp finished_btns
button_4:
; we only want to execute the block if the state changed
lda state_machine
cmp #$05
beq completed_button_4
lda #$00000001 ; Clear display
jsr lcd_instruction
ldx #$0a
lda #$05
sta state_machine
jsr print_string
completed_button_4:
jmp finished_btns
finished_btns:
rts
; 16 bit addition
add16:
lda num1_low
clc
adc num2_low
sta result_low
lda num1_high
adc num2_high
sta result_high
rts
; storing these strings in the upper part of ROM
.org $f000
message_init: .asciiz "Press a key..."
message_btn0: .asciiz "Btn 0 Pressed"
message_btn1: .asciiz "Btn 1 Pressed"
message_btn2: .asciiz "Btn 2 Pressed"
message_btn3: .asciiz "Btn 3 Pressed"
message_btn4: .asciiz "Btn 4 Pressed"
.org $fffc
.word reset
.word $0000
There is definitely a better way to do the implementation, but I wanted to experiment with indirect addressing using the X and Y registers. As an afterthought, it would have been very handy to be able to do something like this:
LDA ($02, X), Y
Although, I realize that this is not possible with the 6502 instruction set.