Large Language Models (LLM) like OpenAI’s ChatGPT are becoming more and more popular and at times more and more useful. One major area that is being explored is using LLMs for coding. There are many examples of people using ChatGPT with many popular languages. Overall, when asking for a general coding task the results are not great, but there is promise. From large companies like such as Microsoft, Google, and Amazon to startups Replit, Tabnine, and many others, the AI-based coding assistant market is changing quickly.

But what about Assembly, and specifically 6502 Assembly? Most of the tools above focus on other languages, but many can be used for basically any language. As you can imagine, without a sharp focus, results will certainly vary. Some have tried to use ChatGPT to do basic ASCII to decimal conversion, implement FizzBuzz, and even write a BASIC interpreter. One person tried to convince ChatGPT to be a 6502. Each attempt produced less than stellar results but does raise a bigger question of how these tools like ChatGPT help us learn and possibly be more productive.

Let’s see if ChatGPT can do something very basic and help us write a 6502 Assembly program that can be run on a W65C02SXB board to blink some LEDs. That shouldn’t be too hard, right? I am giving ChatGPT a bit of information about the board we using, and the fact we are using the VIA to control the LEDs. Will ChatGPT know what a VIA is and how it works?

“Can you write 6502 assembly code for the W65C02SXB board that will blink an LED that is controlled by the W65C22S VIA chip. The base address for the W65C22S VIA chip is 0x7FC0”.

Certainly! Here’s an example of 6502 assembly code that can be used to blink an LED connected to the W65C22S VIA chip on the W65C02SXB board:
; Set up constants
LED_PORT = $7FC0  ; Base address for VIA chip
DDR_B = $7FC2    ; Data Direction Register B
OUT_B = $7FC3    ; Output Register B
PAUSE = $FFFF    ; Delay value for loop

RESET_VECTOR = $FFFC ; Reset vector address

; Initialization routine
INIT:
    SEI             ; Disable interrupts
    LDA #$00       ; Initialize A register with 0
    STA DDR_B      ; Set Data Direction Register B to output mode
    STA LED_PORT   ; Turn off LED initially
    CLI             ; Enable interrupts
    JMP START      ; Jump to start the main loop

; Interrupt handler (not used in this example)
IRQ_HANDLER:
    RTI

; Main program loop
START:
    LDA #$FF       ; Set A register with all 1s
    STA OUT_B      ; Turn on the LED
    JSR DELAY      ; Call the delay subroutine
    LDA #$00       ; Set A register with all 0s
    STA OUT_B      ; Turn off the LED
    JSR DELAY      ; Call the delay subroutine
    JMP START      ; Jump back to the start of the loop

; Subroutine for delay
DELAY:
    LDX PAUSE      ; Load the delay value
LOOP:
    DEX            ; Decrement X register
    BNE LOOP       ; Loop until X becomes zero
    RTS            ; Return from subroutine

; Reset vector
    .ORG RESET_VECTOR
    .WORD INIT     ; Set reset vector to INIT

In this code, the LED is connected to pin B of the W65C22S VIA chip. The code initializes the VIA chip’s data direction register (DDR_B) to set pin B as an output. Then, it enters a main loop where it turns on the LED by setting the output register (OUT_B) to all 1s, waits for a delay using a subroutine, turns off the LED by setting OUT_B to all 0s, and waits for another delay. The delay subroutine decrements the X register until it reaches zero, providing a simple delay mechanism.
Please note that this code assumes a basic setup and initialization of the W65C02SXB board, including proper memory configuration and interrupt handling, which are not included in this code snippet.

So here is the first pass from ChatGPT. It should be noted that this is on the free ChatGPT 3.5. The code spits out in just a few seconds. At first glance, this looks like 6502 code, there is a delay loop and the words LED, DDR, and VIA are there. Aside from the code (which is commented, a nice touch) , note the text that accompanies the code. Having an explanation of what is going on in the code could be helpful for those learning 6502 assemblies or even new to the VIA or W65C02SXB. The explanation tells us that it chose “pin B” of the W65C22S VIA. Pin B is not correct, but assuming that Port B is OK, since it does correctly identify the Data Direction Register for Port B (DDR_B). The rest of the description of the program is OK.

Let’s look at the code! Immediately we can see some issues in the constants setup. Output Register B is not at $7FC3, as the Input and Output Registers for Port B are Register 0. $7FC3 would point to the Data Direction Register for Port A (DDR_A). This is a minor thing to fix but shows that ChatGPT is incorrect in some areas. The PAUSE constant is suspect as it is a 16-bit value. We will see how it is used later. We can keep looking at the code.

Looking at the INIT routine, there are more issues. First, we note that the stack is not initialized, however, that may not be a big concern. ChatGPT did decide to enable interrupts, which is not needed for this simple task. The big issue is setting up the DDR_B and initializing the LEDs. The comments here are even incorrect. We did not specify, so Chat GPT is attempting to set the entire port (all 8 bits) to outputs, so you could actually blink 8 LEDs at once. The DDR is all 0s, to begin with, and actually needs to be written to a 1 to set that bit to an OUTPUT. The STA LED_PORT will work correctly when the Accumulator is 0x00. I asked ChatGPT to correct this mistake for DDR_B and it was happy to oblige.

“Thank you for this. I noticed that you set the Data Direction register to #$00 for OUTPUT, however the outputs are set by writing a 1 to the bit, not a 0. Can you update the code to correct this?”

Apologies for the confusion. You’re right, the W65C22S VIA chip sets outputs by writing a 1 to the corresponding bit. Here’s the updated code with the corrected Data Direction Register (DDR_B) initialization:
; Initialization routine
INIT:
    SEI             ; Disable interrupts
    LDA #$FF       ; Initialize A register with all 1s
    STA DDR_B      ; Set Data Direction Register B to output mode (set bit to 1)
    STA LED_PORT   ; Turn off LED initially
    CLI             ; Enable interrupts
    JMP START      ; Jump to start the main loop

In this updated version, the initialization routine sets the Data Direction Register B (DDR_B) to 1 (output mode) by loading the A register with all 1s (#$FF) and storing it in DDR_B.
Now, when the program runs, the LED will be turned off initially, and then it will alternate between being on and off with the specified delay.

Well, we have the DDR_B correct, however now we are setting LED_PORT (Port B Output Register) to all 1s and turning all of the LEDs on. This is inconsistent with the comment. For now, we will move on as there are other errors. the START uses OUT_B which has the wrong address but for the most part, it does a simple and correct routine. It turns OFF the LEDs, jumps to the DELAY subroutine, turns ON the LEDS, jumps back to the DELAY routine, and finally JuMPs back up to itself to start the whole process over. The DELAY routine unfortunately has a big issue. LDX PAUSE becomes LDX $FFFF. As written, this uses the Absolute Addressing mode and will load the X Index Register with the value at $FFFF. This is not what we want to happen at all for a few reasons. Let’s actually ask ChatGPT to review its work and correct the mistake.

“Thank you for the update. That is better but I can see several more errors. The size of the Index Register X is incorrect. Please review the 6502 microprocessor programming model and correct the code for the appropriate register size for X index register.”

Apologies for the oversight. You’re right, the X register in the 6502 microprocessor is 8 bits (0 to 255), not 16 bits. Here’s the corrected code with the appropriate register size for the X index register:
; Subroutine for delay
DELAY:
    LDX #0xFF      ; Load the delay value
LOOP:
    DEX            ; Decrement X register
    BNE LOOP       ; Loop until X becomes zero
    RTS            ; Return from subroutine

In this corrected version, the delay subroutine sets the X register with the 8-bit value 0xFF (maximum value) to achieve the desired delay. The program will now function properly with the appropriate register size for the X index register.

ChatGPT was able to correctly figure out the side of the X index register. That is fantastic and shows that with some interaction, some things can be improved. Maybe we can have it review the VIA and it will identify the correct internal registers for the Output Register B? Before that, I asked ChatGPT to remove the PAUSE constant that was no longer being used and something unexpected happened.

“Great job. That looks better and the constant “PAUSE” is no longer used so it could be removed.”

You’re absolutely right. Since the “PAUSE” constant is no longer used in the code, we can remove it. Here’s the updated version with the removal of the “PAUSE” constant:
; Set up constants
LED_PORT = $7FC0  ; Base address for VIA chip
DDR_B = $7FC2    ; Data Direction Register B
OUT_B = $7FC3    ; Output Register B
...
...
DELAY:
    LDX #255       ; Load the delay value (8-bit maximum)
...

Now the code has been updated, removing the unused “PAUSE” constant from the code. The LED blinking functionality remains intact, and the program should work as expected.

PAUSE was removed, which is good. The unexpected part is that ChatGPT changed the LDX line in the DELAY loop from LDX #0xFF to LDX #255 and even updated the comment! So the system made a change to a part of the code that I did not directly ask to be changed. Luckily both do the same thing, although some assemblers may have syntax issues with #0x, #255 will always be seen as decimal 255. Okay, now let’s fix this OUT_B constant.

“Thank you. Let’s move on to other errors. There is an error in the OUT_B constant. For the W65C22S, the B port output register is not Register 3. Please review the W65C22 memory map of internal registers and update the code.”

Apologies for the mistake. You’re correct that the output register for Port B on the W65C22S VIA chip is not at offset 3. Based on the W65C22S memory map, the correct offset for Port B’s output register is 4. Here’s the updated code with the correct OUT_B constant:
; Set up constants
LED_PORT = $7FC0  ; Base address for VIA chip
DDR_B = $7FC2    ; Data Direction Register B
OUT_B = $7FC5    ; Output Register B
...

In this updated version, the OUT_B constant has been corrected to reflect the correct offset (5) for the output register of Port B on the W65C22S VIA chip. The code should now properly control the LED using the correct register address.

WAIT! WHAT!?! ChatGPT must be getting tired. Now it is not only incorrect (twice even), but it is inconsistent with the offset. So it was not able to review the internal registers as well as it was the processor programming model. At this point, we are spending more time than necessary and can just make the remaining changes manually. This was a fun exercise to learn more about the capabilities and limitations of the most popular LLM. We can take what we learn and adapt it for future use. The use of templates would be useful to evolve and test various ideas. Here is an example of a template based on our earlier example, making improvements to the initial request. Also, since ChatGPT has a 4.0 option, let’s see what we get when we give the latest version a bit more information.

“Act as a professional software developer Create 6502 assembly code for the W65C02SXB board that will blink an LED that is controlled by the W65C22S VIA chip and use the following as guides: Review the 6502 processor programming model to ensure you use the correct size registers. Starting Address for the program will be at $2000 The LED will be connected to Bit 7 of Port B on the W65C22S The base address for the W65C22S VIA chip is 0x7FC0 The registers for some of the W65C22S VIA chip are below: VIA_BASE: equ $7FC0 ; base address of W65C22S VIA port on SXB VIA_ORB: equ VIA_BASE ; Port B Output Register VIA_IRB: equ VIA_BASE ; Port B Input Register VIA_ORA: equ VIA_BASE+1 ; Port A Output Register VIA_IRA: equ VIA_BASE+1 ; Port A Input Register VIA_DDRB: equ VIA_BASE+2 ; Port B Data Direction Register VIA_DDRA: equ VIA_BASE+3 ; Port A Data Direction Register Review your code for any possible mistakes. Thank you!”


The program will start by setting Bit 7 of Port B Data Direction Register (VIA_DDRB) to be output. Then it will continually write to Port B Output Register (VIA_ORB) to toggle the LED on and off with a delay between each toggle for visibility.
Here is the 6502 assembly code:
    .ORG $2000        ; set the origin to $2000 as specified

VIA_BASE: .EQU $7FC0  ; base address of W65C22S VIA port on SXB
VIA_ORB:  .EQU VIA_BASE      ; Port B Output Register
VIA_DDRB: .EQU VIA_BASE+2    ; Port B Data Direction Register

START:    LDA #$80    ; load the accumulator with 10000000, to set bit 7 as output
          STA VIA_DDRB; set data direction for Port B. Bit 7 is output, rest are input
          
MAINLOOP:  LDA #$80   ; load the accumulator with 10000000, to set bit 7 high (LED on)
           STA VIA_ORB; output to Port B, turning on the LED
           JSR DELAY  ; call delay subroutine
           LDA #$00   ; load the accumulator with 00000000, to set bit 7 low (LED off)
           STA VIA_ORB; output to Port B, turning off the LED
           JSR DELAY  ; call delay subroutine
           JMP MAINLOOP; jump back to start of main loop, repeats the LED blinking

DELAY:     LDX #$00   ; initialize X register as counter
DL:        DEX        ; decrement X
           CPX #$00   ; compare X with 0
           BNE DL     ; branch if not equal, repeating delay if X is not 0
           RTS        ; return from subroutine

    .END

This is a simple program and assumes a suitable delay time based on the 6502’s cycle time and the delay loop. If a different delay time is required, you may need to adjust the code of the DELAY subroutine. Also, this program will run indefinitely until the system is powered off or reset.
In terms of the 6502 processor programming model, the code uses the Accumulator and X index register for loading data and controlling the loop in the DELAY subroutine. The Stack is used for subroutine calls (JSR) and returns (RTS).
Note: The above code assumes the clock frequency and other related hardware considerations. For a real-world scenario, the delay may need to be adjusted according to the specific clock speed of your microprocessor and the exact visual effect you want for the LED blinking.

This is overall a good response. Between giving more clear instructions and using the latest model, we get a much better response. This is something we can keep experimenting with for future tasks. The code is commented and there is some explanation before and after the code. The constants were created correctly and using names given by our input. The correct bit of Port B was selected. The code given can be easily copied into a project and assembled with no errors. There is a note at the end of the explanation about a real-world scenario. Since we are basing this example around a W65C02SXB, let’s give ChatGPT a bit more information and challenge it to create a more precise blinking delay.

“Thank you, that looks pretty good. Can you update the code for a specific clock speed of 8MHz and blink time of 1 second ON and 1 second OFF?”

For an 8 MHz clock speed, each instruction cycle takes 0.125 microseconds (1/8MHz), since the 6502 performs one instruction per clock cycle.
Assuming the LED should be on or off for 1 second, and the delay loop consists of a decrement operation (DEX) taking 2 cycles, a compare operation (CPX) also taking 2 cycles, and a conditional branch operation (BNE) taking 2 cycles (if the branch is taken), then the total cycles taken by the loop is 6 cycles.
We can then calculate how many loops are required for 1 second delay:
1 second / (0.125 microseconds/cycle * 6 cycles/loop) = 1,333,333.33 loops
As the 6502 processor only supports 8-bit operations natively and our loop counter is stored in the 8-bit X register, we can only count up to 255 (0xFF) in a single loop. To achieve a 1 second delay, we need to nest our loops.
We can create an outer loop that executes approximately 5225 times (1,333,333 / 255 ≈ 5225), and each of those will execute the inner loop 255 times. This gives a total of approximately 1,333,375 iterations, which is slightly more than calculated, but the excess time is negligible for this case.
Here is the updated assembly code:
    .ORG $2000

VIA_BASE: .EQU $7FC0  
VIA_ORB:  .EQU VIA_BASE    
VIA_DDRB: .EQU VIA_BASE+2  

START:    LDA #$80
          STA VIA_DDRB
          
MAINLOOP: LDA #$80
          STA VIA_ORB
          LDX #$14   ; outer loop counter
OL:       LDY #$FF   ; inner loop counter
IL:       DEY        ; decrement inner loop counter
          BNE IL     ; branch if not equal to zero, repeating delay
          DEX        ; decrement outer loop counter
          BNE OL     ; branch if not equal to zero, repeating outer loop
          LDA #$00
          STA VIA_ORB
          LDX #$14   ; outer loop counter
OL2:      LDY #$FF   ; inner loop counter
IL2:      DEY        ; decrement inner loop counter
          BNE IL2    ; branch if not equal to zero, repeating delay
          DEX        ; decrement outer loop counter
          BNE OL2    ; branch if not equal to zero, repeating outer loop
          JMP MAINLOOP

    .END

Note: The exact duration might not match perfectly due to various factors such as instruction execution time and possible interruptions by interrupts. If a very precise timing is required, a hardware timer interrupt might be a better choice.

We get a response and breakdown to accomplish what we asked. There are a few things to notice about the response. Notice the change in the comments. Overall the previous response has better explanations in the comments. The code also does not seem to actually correspond with the preceding explanation (no CPX for example). There were changes made to create the inner and outer loops. As noted, this will not provide an exact SECOND of delay and the suggestion for using a timer is a good one. Maybe later we can have ChatGPT use the timers of the VIA for this.

This has been a fun experiment of using ChatGPT to create 6502 Assembly. Initially, there were blatant errors it did not seem very useful, especially for someone just trying to learn some new concepts. By interacting with ChatGPT and improving on our prompting, we certainly get better responses. Feel free to use our template example as a starting place. If you are new to the 6502 and 6502 Assembly, Garth Wilson’s 6502 Primer or Andrew Blance’s Introduction to 6502 Assembly and low-level programming may be a better place to start than ChatGPT.