6502 Assembly Language State Machine

6

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.

Finite-state machine operation.

About the author

Chip Barrere

Chip is an electrical engineer, software developer, and meteorologist.