6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Thu May 09, 2024 8:41 am

All times are UTC




Post new topic Reply to topic  [ 9 posts ] 
Author Message
PostPosted: Mon Sep 26, 2022 10:07 pm 
Offline

Joined: Sat Aug 14, 2021 6:04 pm
Posts: 10
I'm working on an assembly-language project using cc65. Although the project is mostly assembly, I write all the unit tests in C. It's just easier to build unit tests that way. I run the tests under sim65, and the project itself also works under sim65.

Unfortunately sim65 doesn't really have any debugging features. After some experimentation I decided the fastest way to get some visibility into what the program is doing is to just dump the registers out at various points in the program. To do that I created the module below.

The install_debug_handler function points the BRK vector to debug_handler, which dumps out the registers and flags via cc65's standard library function fprintf. You probably know that BRK is actually a two-byte instruction where the second byte is ignored. The debug handler takes advantage of this by printing the byte following the BRK along with the registers and flags; I use the second byte to identify where the program is. I defined a debug macro that just accepts an argument and adds it after the BRK:

Code:
.macro debug value
        brk
        .byte   value
.endmacro


This has been pretty helpful; if one of my unit tests fail then I just sprinkle the debug macro around to see where the program wound up going and what the registers were. I've rarely had to spend more than an hour or so tracking down some problem, so I don't miss having a source-level debugger that much.

Here's the module. Let me know if you find any issues or have suggestions for improvement.

Code:
; MIT License
;
; Copyright (c) 2022 Willis Blackburn
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in all
; copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
; SOFTWARE

; cc65 runtime
.import pushax, pusha0

; C standard library
.import _fprintf, _stderr

; Point the BRK handler to the debug_handler function.

install_debug_handler:
        lda     #<debug_handler
        sta     $FFFE                   ; BRK vector low byte
        lda     #>debug_handler
        sta     $FFFF                   ; BRK vector high byte
        rts

.export install_debug_handler

.zeropage

save_pc: .res 2

.bss

save_a: .res 1
save_x: .res 1
save_y: .res 1
save_sp: .res 1
save_flags: .res 1

flag_indicators: .res 8

.code

format: .byte "$%02X: A=%02X X=%02X Y=%02X SP=%02X %.8s", $0A, $00
flag_names: .byte "NV-BDIZC"

; Prints the register values to stderr.
; Although calling into the C library from an interrupt handler is normally asking for trouble, since sim65
; doesn't generate interrupts, this will only be called by a BRK statement.

debug_handler:
        cld                             ; Clear decimal flag (just in case)
        sta     save_a                  ; Save 6502 registers
        stx     save_x     
        sty     save_y     
        tsx                             ; Get stack pointer into X
        stx     save_sp                 ; Save it so we can print it
        ldy     $102,x                  ; PC low byte
        sty     save_pc     
        ldy     $103,x                  ; PC high byte
        dey                             ; Subtract 256 from PC; we will index with Y = 255 to get PC-1
        sty     save_pc+1       
        lda     $101,x                  ; Flags
        sta     save_flags
        ldy     #0
@next_flag:
        lda     flag_names,y            ; Store name in indicator string if flag on
        rol     save_flags
        bcs     @on
        lda     #'-'                    ; Store '-' indicator string if flag off
@on:
        sta     flag_indicators,y
        iny
        cpy     #8
        bne     @next_flag
        lda     _stderr                 ; fprintf(stderr, ...
        ldx     _stderr+1
        jsr     pushax
        lda     #<format                ; format, ...
        ldx     #>format
        jsr     pushax
        ldy     #$FF
        lda     (save_pc),y             ; id, ...
        jsr     pusha0         
        lda     save_a                  ; A, ...
        jsr     pusha0
        lda     save_x                  ; X, ...
        jsr     pusha0
        lda     save_y                  ; Y, ...
        jsr     pusha0
        lda     save_sp                 ; SP, ...
        jsr     pusha0
        lda     #<flag_indicators       ; flag_indicators)
        ldx     #>flag_indicators
        jsr     pushax           
        ldy     #16                     ; 16 bytes on the C stack
        jsr     _fprintf
        lda     save_a                  ; Restore 6502 registers
        ldx     save_x
        ldy     save_y
        rti


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 27, 2022 1:54 am 
Offline
User avatar

Joined: Tue Mar 05, 2013 4:31 am
Posts: 1373
I also use the BRK instruction for debugging code from time to time. My BIOS actually has a built-in handler for the BRK function. Note that the IRQ/BRK vector points to this code:

Code:
;This front end for the IRQ vector saves the CPU registers and determines if a BRK
; instruction was the cause. There are 25 clock cycles to jump to the IRQ vector,
; and there are 26 clock cycles to jump to the BRK vector. Note that there is an
; additional 18 clock cycles for the IRQ return vector, which restores the registers.
; This creates an overhead of 43 (IRQ) or 44 (BRK) clock cycles, plus whatever the
; ISR or BRK service routines add.
;
IRQ_VECTOR                              ;This is the ROM start for the BRK/IRQ handler
                PHA                     ;Save A Reg (3)
                PHX                     ;Save X Reg (3)
                PHY                     ;Save Y Reg (3)
                TSX                     ;Get Stack pointer (2)
                LDA     $0100+4,X       ;Get Status Register (4)
                AND     #$10            ;Mask for BRK bit set (2)
                BNE     DO_BRK          ;If set, handle BRK (2/3)
                JMP     (IRQVEC0)       ;Jump to Soft vectored IRQ Handler (6)
DO_BRK          JMP     (BRKVEC0)       ;Jump to Soft vectored BRK Handler (6)
;
NMI_ROM         JMP     (NMIVEC0)       ;Jump to Soft vectored NMI handler (6)
;
;This is the standard return for the IRQ/BRK handler routines (18 clock cycles)
;
IRQ_EXIT0       PLY                     ;Restore Y Reg (4)
                PLX                     ;Restore X Reg (4)
                PLA                     ;Restore A Reg (4)
                RTI                     ;Return from IRQ/BRK routine (6)
;


The BRK handler, which is soft vectored via Page $03 is here:

Code:
;
; BRK Vector defaults to here
;
BRKINSTR0       PLY                     ;Restore Y Reg (4)
                PLX                     ;Restore X Reg (4)
                PLA                     ;Restore A Reg (4)
                STA     AREG            ;Save A Reg (3)
                STX     XREG            ;Save X Reg (3)
                STY     YREG            ;Save Y Reg (3)
                PLA                     ;Get Processor Status (4)
                STA     PREG            ;Save in PROCESSOR STATUS preset/result (3)
                TSX                     ;Xfer STACK pointer to X Reg (2)
                STX     SREG            ;Save STACK pointer (3)
;
                PLX                     ;Pull Low RETURN address from STACK then save it (4)
                STX     PCL             ;Store program counter Low byte (3)
                STX     INDEXL          ;Seed Indexl for DIS_LINE (3)
                PLY                     ;Pull High RETURN address from STACK then save it (4)
                STY     PCH             ;Store program counter High byte (3)
                STY     INDEXH          ;Seed Indexh for DIS_LINE (3)
                BBR4    PREG,DO_NULL    ;Check for BRK bit set (5)
;
; The following three subroutines are contained in the base C02 Monitor code. These calls
; do a register display and disassembles the line of code that caused the BRK to occur
;
                JSR     M_PRSTAT1       ;Display CPU status (6)
                JSR     M_DECINDEX      ;Decrement Index to BRK ID Byte (6)
                JSR     M_DECINDEX      ;Decrement Index to BRK instruction (6)
                JSR     M_DIS_LINE      ;Disassemble BRK instruction (6)
;
; Note: This routine only clears Port A, as it is used for the Console
;
DO_NULL         LDA     #$00            ;Clear all Processor Status Register bits (2)
                PHA                     ;Push it to Stack (3)
                PLP                     ;Pull it to Processor Status (4)
                STZ     ITAIL_A         ;Clear input buffer pointers (3)
                STZ     IHEAD_A         ; (3)
                STZ     ICNT_A          ; (3)
                JMP     (BRKRTVEC0)     ;Done BRK service process, re-enter monitor (6)
;


The routines used from the Monitor provide the register contents and the address of the BRK instruction and also disassembles the line of code that caused it.
NOTE: The BIOS calls Monitor routines using "M_" in front of the actual monitor routine label, as they are separate pieces of code.

Code:
;[R] REGISTERS command: Display contents of all preset/result memory locations
PRSTAT          JSR     B_CHROUT        ;Send "R" to terminal
PRSTAT1         LDA     #$13            ;Get Header msg
                JSR     PROMPT          ;Send to terminal
                LDA     PCL             ;Get PC Low byte
                LDY     PCH             ;Get PC High byte
                JSR     PRWORD          ;Print 16-bit word
                JSR     SPC             ;Send 1 space
;
                LDX     #$04            ;Set for count of 4
REGPLOOP        LDA     PREG,X          ;Start with A Reg variable
                JSR     PRBYTE          ;Print it
                JSR     SPC             ;Send 1 space
                DEX                     ;Decrement count
                BNE     REGPLOOP        ;Loop back till all 4 are sent
;
                LDA     PREG            ;Get Status Register preset
                LDX     #$08            ;Get the index count for 8 bits
SREG_LP         ASL     A               ;Shift bit into Carry
                PHA                     ;Save current (shifted) SR value
                LDA     #$30            ;Load an Ascii zero
                ADC     #$00            ;Add zero (with Carry)
                JSR     B_CHROUT        ;Print bit value (0 or 1)
                PLA                     ;Get current (shifted) SR value
                DEX                     ;Decrement bit count
                BNE     SREG_LP         ;Loop back until all 8 printed
                JMP     CROUT           ;Send CR/LF and return to caller
;


Code:
;DECINDEX subroutine: decrement 16 bit variable INDEXL/INDEXH
DECINDEX        LDA     INDEXL          ;Get index low byte
                BNE     SKP_IDXH        ;Test for INDEXL = zero
                DEC     INDEXH          ;Decrement index high byte
SKP_IDXH        DEC     INDEXL          ;Decrement index low byte
                RTS                     ;Return to caller
;


The disassembler code is much larger and wouldn't be practical to include in a post. In any case, simply inserting a BRK opcode (or replacing an existing instruction with BRK) allows quick and easy debugging... for me at least.

_________________
Regards, KM
https://github.com/floobydust


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 27, 2022 2:06 pm 
Offline

Joined: Sat Aug 14, 2021 6:04 pm
Posts: 10
Cool. It's interesting to see how other people do it. Having built-in monitor support is nice; sim65 doesn't have a monitor, unless of course you link one into your program.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 27, 2022 4:08 pm 
Offline
User avatar

Joined: Fri Aug 03, 2018 8:52 am
Posts: 746
Location: Germany
i just do "print debugging", but in the case of assembly i use single characters using this macro:

Code:
.macro   debug   char
   PHP
   PHA
   LDA #char
   STA f:SERIAL
   PLA
   PLP
.endmacro


a BRK based routine seems a lot smarter, i might have to write something like that. so this thread is pretty useful for some ideas on how it could be done.

on a side note, you can tell ca65 to allow special characters like \n, \r, and similar to be used in strings.
you just need to have .feature string_escapes somewhere at the start of your assembly file.
so you can write strings as:
.byte "This is a string with a new\nline in the middle and a null byte at the end!\x00"
instead of having to split them like this:
.byte "This is a string with a new", $0A, "line in the middle and a null byte at the end!", $00
i found this really useful for readability and convenience.


Top
 Profile  
Reply with quote  
PostPosted: Thu Sep 29, 2022 3:37 am 
Offline
User avatar

Joined: Tue Mar 05, 2013 4:31 am
Posts: 1373
It is nice to have the BRK routine... but, as I use a serial port for the console, I use a received break from the terminal program to break out of certain things, like the Macro function of the Monitor. It also tends to get the system out of some odd hangs from other software problems. The BRK routine senses the difference and acts accordingly.

When debugging new code, I can simply replace any byte in the code with a $00 from the Monitor function... which can also write to the EEPROM insitu for same. Adding a signature byte after the BRK makes it easier to figure out how far along the code went before it developed a problem and crashed.

Using this simple and perhaps brute force approach to debugging, I've rarely needed to break out a scope or logic analyzer. Then again, if it's a hardware problem... they come in quite handy.

Another approach I use is to add a couple instructions to store a specific data byte into a specific RAM location (well outside of the code being tested) at specific checkpoints in the code. If it crashes, I have a panic switch (NMI trigger) which when invoked, the NMI routine saves all of the CPU registers and program counter into page zero, then resets the I/O console and vectors, clears out the console buffer and jumps to the Monitor warm start entry. Granted, none of this is truly fancy, but it's a simple approach to finding where code breaks. Also note that this is working on actual hardware, not a simulator.

_________________
Regards, KM
https://github.com/floobydust


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 11, 2022 3:11 pm 
Online
User avatar

Joined: Sun Jun 30, 2013 10:26 pm
Posts: 1928
Location: Sacramento, CA, USA
When I was debugging VTL02, I used AppleWin and the Apple ][ monitor to sprinkle BRKs at various points in my code, then used the monitor facilities to figure out what was up. In my case, it was usually an "off-by-one" in the Y register, which VTL02 uses extensively to access the user program text. The Apple ][ monitor feels easy and natural to me, so I made quick work of the bugs, with the aid of a .lst printout. AppleWin has its own powerful debugger too, but I never learned how to use it.

_________________
Got a kilobyte lying fallow in your 65xx's memory map? Sprinkle some VTL02C on it and see how it grows on you!

Mike B. (about me) (learning how to github)


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 11, 2022 5:53 pm 
Offline

Joined: Wed Jan 08, 2014 3:31 pm
Posts: 575
I use print debugging also. When possible I try to incorporate it into permanent validation functions. For example I wrote a heap module, so one function traverses the heap, prints the allocations, and validates the heap structure. My dynamic array class has a similar feature.

I then write unit tests that use these modules and call the sanity check function. That way none of this effort is throw away work, and is reused each time I modify the module.

A technique I use less often is inserting BRK instructions and manually examining memory to see if the contents match expectations. I do this less often because it's tedious work and essentially a one time check.

Flow control validation is also best done with print debugging. For small assembly projects I just add and remove print statements, which is not reusable effort. However, on large software engineering projects I would use a logging class that can be stubbed out with an alternate version that does nothing.


Top
 Profile  
Reply with quote  
PostPosted: Tue Nov 01, 2022 11:56 am 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
WillisBlackburn wrote:
Unfortunately sim65 doesn't really have any debugging features.

What are you looking for exactly? I wrote a full 65C02 implementation in Swift (https://github.com/nrivard/Microprocessed) that is fully covered by unit tests so you can see how it works. If you are comfortable with Swift, you could write a commandline wrapper around it that should do what you want. You can even simulate your address space exactly as you like. If your needs are pretty small, I would be willing to help write a wrapper as well but not sure what exactly you need.


Top
 Profile  
Reply with quote  
PostPosted: Wed Nov 02, 2022 1:42 pm 
Offline

Joined: Sun May 13, 2018 5:49 pm
Posts: 247
For debugging Tali Forth 2, Scot and I wrote a wrapper (in Python) for py65 that fed files to the input as if they were typed in over the serial port and then saved all of the output. The test files contain the commands needed to invoke a test along with any data that test would expect. See talitest.py at https://github.com/scotws/TaliForth2/tree/master/tests

All of the tests are run and then the Python program looks through the output file for any error messages (that the 65C02 software itself had printed out) and prints those to the screen. To investigate anything further, the output files contain all the commands/data sent to the input as well as the 65C02 software's response. In the end, over 1300 separate tests are run to test every single Forth word that Tali2 comes with (each word tested in multiple ways). This makes it easy to catch if something is broken accidentally while working on new features. It's also convenient to test on real hardware because I can run all of these same tests on real hardware just by copying the test input file over the serial port to the real hardware and use my terminal to log the results to a file.

py65 also lets you capture reads and writes to addresses of your choosing, so it is very easy to add virtual hardware. Many of Tali2's words are also tested for cycle count using a virtual timer that runs at "clock speed". In my case, a read to $F006 starts the timer at 0, a read to $F007 stops the timer, and a read to $F008-$F00B gets the 32-bit value (in the byte order Tali2 expects for a 32-bit value). This allows the software running in the simulator to time itself.

When trouble with a Forth word was found, I would arm myself with the source, assembler listing, and the label map. I'd set a breakpoint at the beginning of that word and single-step through just that word. The labelmap was handy to determine what function a JSR was running, and the listing showed what assembly instructions were being used (which may or may not match what I THOUGHT the source assembly said).

Forth makes it super-easy to diagnose individual words as you can manually run just one word or even quickly write test words and run them, but this method (having the tests run on the 65C02) can be used for any language. You just create a test file with whatever you would type into a real 6502 (commands and data, in the exact order your software expects them) and you get a file with the results that you can look at, analyze, and then make sure your software continues to give that same result in the future (for regression testing) as you continue working on your software.

-SamCoVT


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 9 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 10 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: