Program Counter

Programming the 6502 microprocessor and its relatives in assembly and other languages.
DonnaD
Posts: 33
Joined: 05 Jul 2007

Program Counter

Post by DonnaD »

Can anyone tell me how to read the Program Counter? Is there a memory address somewhere that contains this in the 65C02? I would like to read the current Program Counter position, and store it to a memory address.
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Post by GARTHWILSON »

On the 6502, since JSR puts the return address on the stack, do a JSR to a routine that reads it off the stack and stores it elsewhere before returning. It's a little messy, but it can be done.

On the 65816, the single instruction PER pushes the current address plus an offset in the operand onto the stack, without affecting A, X, Y, or status. If you don't want an offset, just make the operand reflect that fact.

Tip #38 from my "Tip of the Day" column in the old Delphi forum uses this kind of thing to read the program counter. This is an example from a 6502 application I did in the late 1980's.

<begin quote from viewtopic.php?t=342&postdays=0&postorder=asc&start=38 >
Tip of the day, #38

Don't ignore your assembler' more advanced features. Macros especially make assembly code more concise, even hiding the ugly details of conditional assembly based on input parameters or other factors. Since there's no run-time overhead, macros don't slow the execution down like JSR and RTS do, and a macro may be worth defining even if you only use it once. The simple example

Code: Select all

        DISPLAY_IMM  "Press YES key to continue"
        WAIT_FOR_KEY  YESKEY 
could display the message and then wait in a loop until the user presses the Yes/Continue key. The first line, "display immediate", indicates that the parameter is the actual string to display and not just an address. It might assemble

Code: Select all

        JSR   DISP_QUOTE
        BYTE  25          ; (the string length)
        BYTE  "Press YES key to continue"
where the first line calls a subroutine, which uses the return address to pick up the next byte which tells how many bytes following make up the string that is to be displayed, gets the string, and advances the return address past the data to the next actual instruction. The WAIT_FOR_KEY line might assemble

Code: Select all

ReScan: JSR   SCAN_KEYPAD
        CMP   #YESKEY
        BNE   ReScan
Suddenly assembly is a higher-level language than you thought. A macro may replace hundreds of lines. A complex piece of code may be much more clear with certain portions pre-defined as macros with descriptive names. A macro's parameter list can be as long as you want, as long as it all fits on one line of usually 255 characters. Most assemblers allow nested macros. The assembler's list code output file shows exactly what got assembled.
User avatar
dclxvi
Posts: 362
Joined: 11 Mar 2004

Post by dclxvi »

GARTHWILSON wrote:
On the 6502, since JSR puts the return address on the stack, do a JSR to a routine that reads it off the stack and stores it elsewhere before returning. It's a little messy, but it can be done.
You can also use BRK, since it pushes the program counter onto the stack. If the BRK handler is:

Code: Select all

TSX
INX
INX
LDA $100,X ; lo byte of return address
INX
LDY $100,X ; hi byte of return address
RTI
then after:

Code: Select all

HERE: BRK
      .byte $xx ; any value ($00 to $FF) can be used here
A will contain the low byte of HERE+2 and Y will contain the high byte of HERE+2 (and X gets overwritten).

An even more useful BRK handler is:

Code: Select all

      CLC
      TSX
      INX
      INX
      ADC $100,X ; add lo byte of return address
      INX
      LDY $100,X ; hi byte of return address
      BCC SKIP
      INY      
SKIP: RTI
which returns HERE+2+A in A (low byte) and Y (high byte).

The equivalent for JSR is:

Code: Select all

ENTRY1: CLC
ENTRY2: TSX
        INX
        ADC $100,X ; add lo byte of return address
        INX
        LDY $100,X ; hi byte of return address
        BCC SKIP
        INY      
SKIP:   RTS
then:

Code: Select all

HERE: JSR ENTRY1
returns HERE+2+A in A (low byte) and Y (high byte). (Using JSR ENTRY2 returns HERE+2+A+C).
bogax
Posts: 250
Joined: 18 Nov 2003

Post by bogax »

dclxvi wrote:

Code: Select all

TSX
INX
INX
LDA $100,X ; lo byte of return address
INX
LDY $100,X ; hi byte of return address
RTI

do you really need all those INX instructions?
couldn't you just build it into the address?

Code: Select all

 TSX
 LDA $102,X ; lo byte of return address
 LDY $103,X ; hi byte of return address
 RTI
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Post by GARTHWILSON »

That will work as long as you initialized the stack pointer at startup so you won't have a situation where for example $103 + X can get you into page 2 instead of wrapping around to the other end of page 1 where the stack is.
DonnaD
Posts: 33
Joined: 05 Jul 2007

Post by DonnaD »

If BRK puts the PC on the stack, can't I just PLA from both the hi byte and the lo byte off the stack and just store them to memory?

Code: Select all

   PLA		;Pull Program Counter Low from stack
	STA pchi	;store to pchi
	PLA		;Pull Program Couter High from stack
	STA pclo	;store to pclo
	PHA		;Push pclo back on stack
	LDA pchi
	PHA      ;Push pchi back on stack	
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Post by GARTHWILSON »

You could, if you later use that to get back to where the BRK routine was called; but you can't just do an RTI after you've removed the address from the stack. You would also have to use another instruction to clear the interrupt-disable bit, and probably also check to see if it even should be cleared.

I wouldn't use the BRK instruction though, for a couple of reasons. For one, it requires the interrupt handler to check the B flag to see if it was called by a software or hardware interrupt. (The 65816 does not have this problem as it has a separate BRK vector though.) That adds to the overhead, even for hardware interrupts. For another, BRK sets the interrupt-disable bit I, meaning that interrupts will get delayed unnecessarily, and then you again run into the I bit restoration business mentioned above.

I prefer to use JSR, with the TSX and LDA 102,X etc. in the subroutine which ends with RTS. I haven't ever used BRK since I was on the AIM-65 trainer computers in school over 25 years ago.
User avatar
dclxvi
Posts: 362
Joined: 11 Mar 2004

Post by dclxvi »

DonnaD wrote:
If BRK puts the PC on the stack, can't I just PLA from both the hi byte and the lo byte off the stack and just store them to memory?
BRK pushes P onto the stack AFTER pushing the return address onto the stack, so P is on the top of the stack. You'd have to add a PLP to the beginning of your routine and a PHP to the end; it's a little slower than using $100,X, but it works.
GARTHWILSON wrote:
I wouldn't use the BRK instruction though, for a couple of reasons.
I agree, but I would state this as: I wouldn't use both BRK and IRQ, unless neither one needs to be fast. I like BRK a lot, but I don't use IRQ much.

The (slight) advantage of BRK over JSR is that with JSR, you have to know the address of the where-am-I routine beforehand (at assemble/compile time), which isn't the case for BRK. This probably won't be a major issue, though.
DonnaD
Posts: 33
Joined: 05 Jul 2007

Post by DonnaD »

I have a simple ml monitor that is the BRK handler. So the monitor is running as the BRK handler and I would like to get the PC from where the BRK was issued.
User avatar
8BIT
Posts: 1787
Joined: 30 Aug 2002
Location: Sacramento, CA
Contact:

Post by 8BIT »

DonnaD,

Here is the Break Handler I use in my SBC-2 monitor.

ACC, Xreg, Yreg, Preg, PCH, PCL, SPtr are all storage locations in page 3 that the PrintReg routine uses to dump the registers after a break.

Code: Select all

BRKroutine     sta   ACC               ; save A    Monitor"s break handler
               stx   Xreg              ; save X
               sty   Yreg              ; save Y
               pla                     ; 
               sta   Preg              ; save P
               pla                     ; PCL
               plx                     ; PCH
               sec                     ;
               sbc   #$02              ;
               sta   PCL               ; backup to BRK cmd
               bcs   Brk2              ;
               dex                     ;
Brk2           stx   PCH               ; save PC
               TSX                     ; get stack pointer
               stx   SPtr              ; save stack pointer
               jsr   Bell              ; Beep speaker
               jsr   PrintReg          ; dump register contents 
               ldx   #$FF              ; 
               txs                     ; clear stack
               cli                     ; enable interrupts again
               jmp   Monitor           ; start the monitor

The code below is my PrintReg routine.

Code: Select all

RegData        .byte" PC=  A=  X=  Y=  S=  P= (NVRBDIZC)="
;
PrintReg       Jsr   Print_CR          ; Lead with a CR
               ldx   #$ff              ;
               ldy   #$ff              ;
Printreg1      iny                     ;
               lda   Regdata,y         ;
               jsr   Output            ;
               cmp   #$3D              ; "="
               bne   Printreg1         ;
Printreg2      inx                     ;
               cpx   #$07              ;
               beq   Printreg3         ; done with first 6
               lda   PCH,x             ;  
               jsr   Print1Byte        ;
               cpx   #$00              ;
               bne   Printreg1         ;
               bra   Printreg2         ;
Printreg3      dex                     ;
               lda   PCH,x             ; get Preg
               ldx   #$08              ; 
Printreg4      rol                     ;
               tay                     ;
               lda   #$31              ;
               bcs   Printreg5         ;
               dec                     ;
Printreg5      jsr   Output            ;
               tya                     ;
               dex                     ;
               bne   Printreg4         ;
; fall into the print CR routine
Print_CR       PHA                     ; Save Acc
               LDA   #$0D              ; "cr"
               JSR   OUTPUT            ; send it
               LDA   #$0A              ; "lf"
               JSR   OUTPUT            ; send it
               PLA                     ; Restore Acc
               RTS                     ; 
The code below is the IRQ handler that decides if its an IRQ or BRK.

Code: Select all

Interrupt      PHX                     ;
               PHA                     ;
               TSX                     ; get stack pointer
               LDA   $0103,X           ; load INT-P Reg off stack
               AND   #$10              ; mask BRK
               BNE   BrkCmd            ; BRK CMD
               PLA                     ;
               PLX                     ;
               jmp   (INTvector)       ; let user routine have it 
BrkCmd         pla                     ;
               plx                     ;
               jmp   (BRKvector)       ; patch in user BRK routine
Hope this helps...

Daryl
DonnaD
Posts: 33
Joined: 05 Jul 2007

Post by DonnaD »

Thanks Daryl,

This helped a lot. Can I still use RTI at the end of the BRK routine? Or should I?

I just figured out I can CLI and then RTI..... thanks
kc5tja
Posts: 1706
Joined: 04 Jan 2003

Post by kc5tja »

Holy criminey, how easily people fall into the complexity trap.

Code: Select all

JSR *+3
PLA
STA pclow
PLA
STA pchigh
This will store the address of the first PLA above, less one. If you're doing pointer arithmetic based on this, just remember to add one to your results to get a true pointer. If you're using the PC as a pointer via direct-page addressing, you can often avoid even this, by pre-loading Y with 1 first.

Code: Select all

LDY #$01
LDA (pclow),Y
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Post by GARTHWILSON »

I like that! Yeah, I always got more complex, because the subroutines were called from many places in the code and did more than just get the address, like fetch variable-length data embedded in the code, handle appropriately, then calculate where to return to so as to skip the data and land on the next actual instruction.
Last edited by GARTHWILSON on Wed Feb 20, 2008 6:16 pm, edited 1 time in total.
kc5tja
Posts: 1706
Joined: 04 Jan 2003

Post by kc5tja »

Well, that's doable too. :-)

Code: Select all

  jsr myPrimm ; the hello-world of inline data subroutines, I guess.
  .byte "C:\WINDOWS\> RUN; RUN WINDOWS RUN; AWW, C'MON RUN",13,10,0

; one of MANY ways of implementing PRIMM.
.proc myPrimm
  pla
  sta rpc
  pla
  sta rpc+1

  ldy #1
again:
  lda (rpc),y
  beq done
  jsr chrout
  iny
  jmp again

done:
  iny
  tya
  clc
  adc rpc
  sta rpc
  lda rpc+1
  adc #0
  pha
  lda rpc
  pha
  rts
.endproc
User avatar
dclxvi
Posts: 362
Joined: 11 Mar 2004

Post by dclxvi »

kc5tja wrote:
Holy criminey, how easily people fall into the complexity trap.

Code: Select all

JSR *+3
PLA
STA pclow
PLA
STA pchigh
Um, generally, the where-am-I routine is useful when the code is not at a known location until run time, and the code needs to know where it is so it can adjust accordingly. For example, if the OS only loads the application, but does not provide relocation, and it is up to the application to relocate itself, then the application needs some way to determine where it is, so that it can adjust the absolute addresses that it needs to. In this case, the JSR version of the where-am-I routine would have to be at a known location (e.g. in ROM, rather than being part of the application). However, the *+3 of JSR *+3 is an absolute address (since JSR uses an absolute, rather than a relative address) and would have to be adjusted before it is encountered.

If the location is known at assembly (or link) time, why wouldn't you just use:

Code: Select all

here: LDA #here
      STA pclo
      LDA #>here
      STA pchi
(or here+constant). Any half-decent 6502 linker ought to be able to handle that.

The way I interpreted the original post is that the goal could be stated as: determining "where am I?" in an application, rather than a subroutine within the application determining "where was I called from?". However, what you're suggesting will work just fine for PRIMM, when overwriting A and the zp pointer are acceptable. If I've misinterpreted the question, then feel free to ignore my ramblings. :)
Post Reply