thecodingidiot.com

Writing 6502 AssemblyCounter and LED

Counter and LED

Two programs. The first writes nothing to the VIA — it just increments a byte in RAM, forever, as a sanity check that the new address decode works and that RAM is still at 00000000–3FFF after rewiring CE# to A14. The second writes to the VIA for the first time and lights a LED.

counter.s

  .org $8000
 
reset:
  lda #$00
  sta $0000       ; initialise RAM[0] to 0
 
loop:
  inc $0000       ; increment RAM[0]
  jmp loop        ; repeat
 
  .org $fffc
  .word reset
  .word reset

Walking through it line by line.

.org $8000 tells the assembler that the code which follows lives at address $8000 in the CPU's address space. The binary it produces will be padded so that the bytes assemble at the right offsets. When the EEPROM is placed at $8000–$FFFF in the circuit, the CPU fetches instructions from the correct addresses.

lda #$00 loads the literal value zero into A. The # means immediate — the zero is right there in the instruction, not fetched from memory.

sta $0000 stores A (currently zero) into the byte at address $0000. This is RAM, now that A14 is the chip-select and $0000 has A14=0. The instruction initialises the counter to zero on startup.

inc $0000 is the heart of the loop. It reads the byte at $0000, adds 1 to it, and writes the result back. Five cycles total: two to fetch the opcode and the address byte, one to read the old value, one to increment, one to write the new value. On a 1 MHz clock, the loop body executes around 125,000 times per second.

jmp loop sets PC back to the loop label unconditionally. The CPU never stops incrementing.

The .org $fffc / .word reset / .word reset block is the reset vector. When the 6502 is powered on or reset, it reads the two bytes at $FFFC–$FFFD, interprets them as a 16-bit address (low byte first), and jumps there. Both the reset vector ($FFFC) and the IRQ/BRK vector ($FFFE) are set to reset — the program does not use interrupts, so the IRQ vector pointing at reset is harmless and keeps the code short.

Assemble and burn

vasm6502_oldstyle -Fbin -dotdir counter.s -o counter.bin
minipro -p AT28C256 -w counter.bin

-Fbin produces a flat binary — no headers, just raw bytes starting at $8000 offset into the 32KB file. -dotdir enables the dot-prefix directives (.org, .word).

Running counter.s

Power the board. The address-line LEDs will change more rapidly than they did with the bare jmp reset test program from f03b, because inc $0000 forces a RAM access on every cycle rather than just fetching a three-byte jump instruction. The pattern on the LEDs advances faster.

At 1 MHz the counter wraps through 0–255 so quickly you cannot read it directly from address LEDs. A logic probe or a slow clock (a manual clock button works well) lets you step through one increment at a time and watch A0 toggle on every other write cycle. The important thing is that the CPU is not stuck and RAM is responding — if it were not, the address lines would show a different pattern, or the CPU would crash when it tried to read back the incremented value.

led.s

Now use the VIA. The program configures all eight Port B pins as outputs and drives PB0 high, which lights a LED.

PORTB = $4000
DDRB  = $4002
 
  .org $8000
 
reset:
  lda #$ff
  sta DDRB        ; all PORTB pins = output
  lda #$01
  sta PORTB       ; PB0 high — LED on
 
loop:
  jmp loop
 
  .org $fffc
  .word reset
  .word reset

PORTB = $4000 and DDRB = $4002 are assembler constants, not instructions. They give human-readable names to the VIA register addresses. PORTB ($4000) is the Port B data register — write a byte there and the corresponding VIA pins take on those logic levels. DDRB ($4002) is the Data Direction Register for Port B — each bit controls whether the matching pin is an output (1) or an input (0).

lda #$ff loads $FF (all eight bits set) into A.

sta DDRB writes $FF to the DDRB register at $4002. With every bit set to 1, every Port B pin is configured as an output. The VIA now drives all eight PB0–PB7 lines.

lda #$01 loads $01 into A. In binary: %00000001 — only bit 0 set.

sta PORTB writes $01 to the PORTB register at $4000. The VIA drives PB0 high (bit 0 = 1) and the remaining seven pins low (bits 1–7 = 0). A LED connected to PB0 sees a voltage difference across it and lights.

jmp loop loops forever. Nothing else needs to happen — the VIA holds the output level until the CPU writes a new value or the power is removed.

Wiring the LED

Connect a 1kΩ resistor from PB0 to the anode (longer leg) of a standard LED. Connect the cathode (shorter leg) to the ground rail. The resistor limits current to a safe level for both the LED and the VIA output driver.

Assemble, burn, verify

vasm6502_oldstyle -Fbin -dotdir led.s -o led.bin
minipro -p AT28C256 -w led.bin

Power the board. The LED should light immediately — there is no startup delay, no loop condition to meet. The CPU writes $FF to DDRB and $01 to PORTB in the first few instructions, then parks itself in jmp loop. If the LED does not light, check the DDRB and PORTB wiring (RS0–RS3 on A0–A3, CS1 on A14, CS2B on A15) before assuming a code problem.