6502/65816 CPU variant detection code

Programming the 6502 microprocessor and its relatives in assembly and other languages.
Post Reply
daivox
Posts: 30
Joined: 04 Sep 2004
Location: Last Ninja 2: Basement

6502/65816 CPU variant detection code

Post by daivox »

I just spent a few hours researching some of the differences between 6500 family variants and writing a short piece of code that should be able to detect what kind of CPU you're running on top of. I've not tested it thoroughly! It works as expected on a C64 (result of $0c meaning normal NMOS 6502 core) and an NES emulator ($04 meaning NMOS 6502 with no decimal mode) but that's the extent of my testing as I either lack supporting code, knowledge, or access to the platforms in question to test every possibility.

It currently supports detection of these 6500 variants, without crashing (hopefully) during any part of detection:

"Stock" NMOS 6502
WDC 65C02 (and clones)
65816
Very early 6502 with the "ROR bug"
Ricoh 2A03 (NES CPU)
Rockwell R65C02 series (with RMB/SMB opcodes)
Hudson Soft HuC6280 (PC Engine CPU)
Mitsubishi/Renesas 740 MPU/MCU
Renesas 740 MUL/DIV instruction support

It's designed to be built with the ACME assembler, version 0.93 or up, and builds to 96 bytes in size. The result of the tests is stored as flags in a zero-page location which can be easily modified, and testing requires at most two zero-page locations beyond the flags storage location. I hope that someone here finds this little code snippet useful.

Code: Select all

!to "detect6502.o",plain
!cpu 65816
*=$4000

zp0=$fb
zp1=$fc
zp2=$fd

; Detects the CPU in use
; 76543210
; ||||||''-- 0=NMOS 6502 (or 65816 if bit 1 ON), 1=65C02 or variant
; ||||||'--- 0=6502 series, 1=65816 series
; |||||'---- 0=Early 6502 w/bad ROR instruction, 1=ROR is normal
; ||||'----- 0=No decimal mode (Ricoh 2A03), 1=decimal mode OK
; |||'------ 1=Hudson Soft HuC6280 CPU
; ||'------- 1=Rockwell R65C02 series CPU
; |'-------- 1=Mitsubishi/Renesas 740 CPU
; '--------- 1=Renesas 740 with MUL/DIV instructions

detect6502
	lda #$02	; Initialize for 740 MUL/DIV test
	sta zp2
; Test for NMOS/CMOS and 6502/65816
	lda #$00
	sta zp1		; Initialize for 740 series test
	inc
	cmp #$01
	bmi +
	xba
	dec
	xba
	inc
+	sta zp0		; 0=6502, 1=65C02, 2=65C816
; Test for very early 6502s with bad ROR instruction
	lda #$08
	clc
	ror
	cmp #$04
	bne +		; 6502 with bad ROR instruction
	ora zp0
	sta zp0		; 4 = good ROR
; Test for Ricoh 2A03 (NES) which has no decimal mode
+	cli		; C64 KERNAL can't handle D set on IRQ
	sed
	lda #$09
	clc
	adc #$01
	cmp #$10
	bne +		; Decimal mode failed
	lsr		; $10 -> $08
	ora zp0
	sta zp0
; Test for Hudson Soft HuC6280 CPU
+	cld
	sei
	and #$01	; Skip remaining tests if not CMOS
	beq ++
	php
	ldy #$01
	rep #$08	; HuC6280 sees "CLY,PHP" instead
	cpy #$00
	bne +		; Not a HuC6280 CPU
	plp		; Undo the PHP instruction
	lda #$20
	ora zp0
	sta zp0		; Set HuC6280 flag
; Test for Rockwell R65C00 series
; This is pitifully simple: it sets the flag bit for us!
+	!byte $d7	; SMB5 opcode on R65C00, ignored on 65C02
; Test for Mitsubishi/Renesas 740 series CPU
	!byte $3c	; LDM on 740, BIT on 65C02 
	!byte $40,zp1	; 740 sets zp1 to $40
	lda zp1
	beq ++		; If not a 740, end testing
	ora zp0		; Should already contain $40
	sta zp0
	lda #$02
	!byte $62,zp2	; MUL zp2
	cmp #$04	; If MUL is supported, 2*2=4
	bne ++
	lda #$80	; Set 740 MUL/DIV flag
	ora zp0
	sta zp0
++	rts
Last edited by daivox on Tue Nov 22, 2022 5:09 pm, edited 1 time in total.
User avatar
BigDumbDinosaur
Posts: 9427
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: 6502/65816 CPU variant detection code

Post by BigDumbDinosaur »

Detecting a 65C816 would be problematic because at reset, it comes up as a (mostly) 65C02 and can only be switched to native mode with the XCE instruction. The XCE opcode is $FB, which would behave as a one byte NOP with a 65C02 but would be undefined on all other eight bit MPUs and might result in a crash.

So I think the path to determining if the MPU is an '816 would be to first determine if it is a 65C02. If it is then you could proceed as follows:

Code: Select all

         clc
         xce
         bcs is_816
         ...

Of course, you'd need an actual 65C816 machine to try out this code.

Code: Select all

; Test for Rockwell R65C00 series
; This is pitifully simple: it sets the flag bit for us!
+   !byte $d7   ; SMB5 opcode on R65C00, ignored on 65C02
$D7 is valid on the WDC 65C02 as SMB 5,ZP.
Last edited by BigDumbDinosaur on Sun Aug 27, 2023 8:01 pm, edited 1 time in total.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
daivox
Posts: 30
Joined: 04 Sep 2004
Location: Last Ninja 2: Basement

Re: 6502/65816 CPU variant detection code

Post by daivox »

The code already checks for 65816 by first checking for 65C02, and only then using XBA instructions to determine if it's a 65816. As for the SMB5 instruction, you are indeed correct; I've modified the comments to indicate that the detection finds "old" vs. "new" type 65C02 chips. Apparently some 65C02 chips exist without SMB/RMB/BBS/BBR opcodes, and the test checks that. R65C02 and WDC 65C02 appear to have identical opcode matrices, but a datasheet on this site for a California Micro Devices G65SC02 has a matrix with these opcode slots empty. Also, I found a bug where I didn't put zp0 in the SMB5 instruction, and fixed it. This changes the code size to 97 bytes.

I also added a warning, because zp0 will be executed as a 65C02 opcode if the SMB5 opcode test fails. Here's the modified code:

Code: Select all

!to "detect6502.o",plain
!cpu 65816
*=$4000

; WARNING: the value in zp0 may be executed as an opcode on CMOS 6502
; chips during the bit-op test. Storing the result in an address that
; consitutes the wrong opcode could result in a crash (for example, if
; $40 is used, a 65C02 without bit-op extensions will execute RTI!)
; Two- or three-byte opcodes in zp0 will cause trouble as well!
; Anything ending in $x3 or $xB will be interpreted as NOP and should
; be safe.
zp0=$fb
zp1=$fc
zp2=$fd

; Detects the CPU in use
; 76543210
; ||||||''-- 0=NMOS 6502 (or 65816 if bit 1 ON), 1=65C02 or variant
; ||||||'--- 0=6502 series, 1=65816 series
; |||||'---- 0=Early 6502 w/bad ROR instruction, 1=ROR is normal
; ||||'----- 0=No decimal mode (Ricoh 2A03), 1=decimal mode OK
; |||'------ 1=Hudson Soft HuC6280 CPU
; ||'------- 1=Bit-op capable 65C02 (RMB/SMB/BBR/BBS opcodes available)
; |'-------- 1=Mitsubishi/Renesas 740 CPU
; '--------- 1=Renesas 740 with MUL/DIV instructions

detect6502
	lda #$02	; Initialize for 740 MUL/DIV test
	sta zp2
; Test for NMOS/CMOS and 6502/65816
	lda #$00
	sta zp1		; Initialize for 740 series test
	inc
	cmp #$01
	bmi +
	xba
	dec
	xba
	inc
+	sta zp0		; 0=6502, 1=65C02, 2=65C816
; Test for very early 6502s with bad ROR instruction
	lda #$08
	clc
	ror
	cmp #$04
	bne +		; 6502 with bad ROR instruction
	ora zp0
	sta zp0		; 4 = good ROR
; Test for Ricoh 2A03 (NES) which has no decimal mode
+	cli		; C64 KERNAL can't handle D set on IRQ
	sed
	lda #$09
	clc
	adc #$01
	cmp #$10
	bne +		; Decimal mode failed
	lsr		; $10 -> $08
	ora zp0
	sta zp0
; Test for Hudson Soft HuC6280 CPU
+	cld
	sei
	and #$01	; Skip remaining tests if not CMOS
	beq ++
	php
	ldy #$01
	rep #$08	; HuC6280 sees "CLY,PHP" instead
	cpy #$00
	bne +		; Not a HuC6280 CPU
	plp		; Undo the PHP instruction
	lda #$20
	ora zp0
	sta zp0		; Set HuC6280 flag
; Test for 65C02 with bit-op instructions
; This is pitifully simple: it sets the flag bit for us!
+	!byte $d7,zp0	; WARNING: zp0 becomes opcode if SMB5 not usable!
; Test for Mitsubishi/Renesas 740 series CPU
	!byte $3c	; LDM on 740, BIT on 65C02 
	!byte $40,zp1	; 740 sets zp1 to $40
	lda zp1
	beq ++		; If not a 740, end testing
	ora zp0		; Should already contain $40
	sta zp0
	lda #$02
	!byte $62,zp2	; MUL zp2
	cmp #$04	; If MUL is supported, 2*2=4
	bne ++
	lda #$80	; Set 740 MUL/DIV flag
	ora zp0
	sta zp0
++	rts
User avatar
BigDumbDinosaur
Posts: 9427
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: 6502/65816 CPU variant detection code

Post by BigDumbDinosaur »

daivox wrote:
The code already checks for 65816 by first checking for 65C02, and only then using XBA instructions to determine if it's a 65816.

Yes, I can see where that would work, and it does have the advantage of not switching modes if the MPU is a 65C816—it stays in emulation mode following the test, although switching it back following an
XCE is a one byte operation.
Last edited by BigDumbDinosaur on Sun Aug 27, 2023 8:03 pm, edited 1 time in total.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
Chromatix
Posts: 1462
Joined: 21 May 2018

Re: 6502/65816 CPU variant detection code

Post by Chromatix »

Performing thread necromancy to point out possibly the single easiest way to distinguish NMOS 6502s from anything later:

Code: Select all

 BRA cmos
nmos:
 [...]
cmos:
This works because $80 (BRA) is an undocumented 2-byte NOP on NMOS. Now, whether emulators implement that quirk or not is an open question, but if you *know* you're running on real hardware…
jgharston
Posts: 181
Joined: 22 Feb 2004

Re: 6502/65816 CPU variant detection code

Post by jgharston »

Digging up this thread, I've been trying to make so code to detect if I'm running on a 65816 or not. I don't need the whole suite of finding everything just is-65816 vs not-65816 (but with a complication that it could be a 6502 or a 65C02/12).

With some experimenting, I've come up with this. Does it look robust enough?

Code: Select all

          ; 6502  A   65C02  A   65816  B   A
 CLC
 LDA #$00 ;       00         00         zz  00
 DB  #$EB ; SBC       NOP    00  XBA    00  zz
 DB  #$3A ; #$3A  C5  DEC A  FF  DEC A  00  yy
 DB  #$EB ; SBC       NOP    FF  XBA    yy  00
 DB  #$EA ; #$EA  DA  NOP    FF  NOP    yy  00
Distinct values for is-65816 and not-65816 (with a added 6502/65C02 that I don't need for this task).

So I can then BEQ m816:BNE m02.
SpottedGal
Posts: 19
Joined: 15 Dec 2019

Re: 6502/65816 CPU variant detection code

Post by SpottedGal »

What about detecting a Sally 6502? I imagine it would test as a stock 6502 since MOS made it for Atari. Really, that just adds back a feature that MOS removed since the original die left room for multiplexers to take the 6502 off the bus. That also explains one of the NC pins. I guess they didn't feel like anyone needed to use bus-mastering DMA.

Sally was a modified 6502, B revision, despite it being listed as a C revision in Atari literature.
User avatar
Sheep64
In Memoriam
Posts: 311
Joined: 11 Aug 2020
Location: A magnetic field

Re: 6502/65816 CPU variant detection code

Post by Sheep64 »

In response to jgharston, testing NMOS/CMOS/65816 is very easy. It is only complicated if you wish to cover obscure cases. As noted by Chromatix, NMOS is unable to follow BRA and this separates it from everything else. 65816 in emulation mode is only faithful to 65C02 if 65C02 instructions are executed. The extended instructions are always available. Although, register sizes are restricted in emulation mode. In response to jeffythedragonslayer (and being needlessly lambasted again by BigDumbDinosaur), and after considering the use of 65816 REP to clear multiple flags simultaneously (decimal, carry, interrupt), I find that REP can also be used to determine execution on 65816. This works in emulation mode or native mode.

SEC // REP #1 will clear carry on 65816 but leave carry set on 65C02. Unfortunately, while BRA ... SEC // REP #1 // BCC ... splits NMOS/CMOS/65816 into three chronological cases, it fails horribly on obscure processor variants, such as 65CE02. In this case, 65CE02 follows BRA. However, REP and SEP are interpreted as direct page, 16 bit increment or decrement (DEW and INW). The immediate value of REP is interpreted as the lower byte of the 16 bit decrement. Thankfully, carry is unaffected. However, DEW $01 may be incompatible with your operating system's allocation of direct page. In this case, REP can be used with higher values. On 65816, it will clear additional flags. On 65CE02, it will decrement at a higher address. Specifically: +$80 to clear negative flag, +$40 to clear overflow flag, +$08 to clear decimal flag. Or you can get fancy with +$04 to begin an atomic operation. Unfortunately, it is highly inadvisable to use +$20 to set accumulator to 16 bit or +$10 to set index registers to 16 bit because 65816 native mode cannot be assumed.

Do not abbreviate cases by omitting BRA. NMOS/65816 test seems very concise and convenient but it will cause chaos on 6510 and 6509 where REP #1 is illegal instruction LAX (dp,X) and simultaneously loads RegA and RegX. This leaves carry unchanged but may use memory location $0001 as the lower byte of a source address. On 6510 and 6509, LAX ($01,X) may strobe a bank latch before possibly strobing I/O. It is quicker and easier to test BRA rather than guard LAX (dp,X). It also preserves RegA and RegX across all three cases.

The obvious 65816 test is to invoke XCE. That is great if the test passes. However, XCE on NMOS becomes the three byte illegal instruction ISC abs,Y which is INC followed by SBC. In this case, XCE // NOP // NOP will become ISC $EAEA,Y. Worse, XCE on 65CE02 becomes PLZ (pull top of stack to RegZ). Overcoming that side-effect requires PHA // TSX // CLC // XCE // NOP // NOP // TXS // PLA // BCS which is longer, slower, requires two byte instruction sequence which is also a safe address region, destroys the contents of RegX, fails to distinguish NMOS/CMOS (which is partly why we do not use PHX/PLX and preserve RegX) and lacks the option to clear flags on 65816. XCE is otherwise a good choice.

Detecting CMOS/65816 with XBA // DEC // XBA is cunning but it doesn't generalize well. XBA on NMOS becomes the three byte illegal instruction DCP abs,Y which is DEC followed by CMP. This requires two instances of two byte instruction sequence which are also safe address regions. XBA on 65CE02 becomes an abs, 16 bit shift operation.
kinzi
Posts: 2
Joined: 17 May 2023

Re: 6502/65816 CPU variant detection code

Post by kinzi »

Hello,

I have designed a small PCB containing a bit of RAM, ROM and a LED interface to run this nice chunk of code checking what 6502 CPU type might disguise in Chinese re-labelled 6502 fake CPUs.

See details on https://www.forum64.de/index.php?thread ... 6502-cpus/

The forum is in German, but the images and the ZIP release file (Gerbers, code, docs, etc.) will be of use for English speaking individuals, too. :-)

After a long while, I had a look at the code again, and I am wondering if the part regarding the detection of Hudson Hu6280 is correct:

Code: Select all

                    ldy #$01
                    rep #$08        ; HuC6280 sees "CLY,PHP" instead
                    cpy #$00
                    bne test_740    ; Not a HuC6280 CPU
                    plp             ; Undo the PHP instruction
                    lda #$20
                    ora zp0
                    sta zp0         ; Set HuC6280 flag
                                    ; Test for 65C02 with bit-op instructions
                                    ; This is pitifully simple: it sets the flag bit for us!
test_740            !byte $d7,zp0   ; WARNING: zp0 becomes opcode if SMB5 not usable!
This sets bit 5 to 1 which is not correct, I think. That bit is set later via SMB5 on if a R65C02 CPU is detected. The "Hudson Bit" is bit #4, so the parameter for the LDA should be #$10, not #$20 - or am I missing something?

Regards
kinzi
Chromatix
Posts: 1462
Joined: 21 May 2018

Re: 6502/65816 CPU variant detection code

Post by Chromatix »

Also relevant to readers of this thread: viewtopic.php?f=2&t=5931

Since the HuC6280 and 65CE02 both interpret opcode $47 the same way as the Rockwell-extended 65C02, they will be identified as one by my code from that thread (returning ASCII 'C'). If you have reasonably free use of zero-page, I recommend running my code first, using the magic $47 opcode to distinguish between NMOS 'N', CMOS 'S', Rockwell-extended 'C', and 65816 '8' variants. If it comes out as CMOS, run the further test using BRA to check for emulators which treat all undocumented NMOS opcodes as 1-byte NOPs. If it comes out as Rockwell-extended, perform additional tests for the HuC6280 and/or 65CE02. You can then omit the additional test for the Rockwell instructions at the end of your routine.

Provided the presence of Rockwell instructions has already been verified, using REP #$08 to distinguish the HuC6280 looks like it would work. This instruction is harmless on the 65CE02, which interprets it as CPZ #imm. A convenient way to test for the 65CE02 as well would be to insert an CPY #1 between the LDY and REP, and a BNE between the REP and CPY. The 65CE02 initialises its extra Z register to zero, so comparing it to 8 will clear the Zero flag, whereas both the alternatives will have the Zero flag set at that point.

However, REP is opcode $C2 which a Renesas 740 would interpret as WIT (wait for interrupt), and furthermore the Rockwell opcodes are rearranged so that $47 in my first-line test would be interpreted as a 3-byte instruction BBS2 zp,rel. This is highly inconvenient, and implies that I have to put the test for a 740-series first of all.

Putting all of this together:

Code: Select all

.P65C02
TestCPU:
  LDA #0
  STA $84
  STA $85
  LDA #$1D ; 'N' EOR 'S'
  STA $83
  LDA #$6B ; 'N' EOR 'S' EOR '8'
  STA $1D
  LDA #$4E ; 'N'
  RMB4 $83 ; magic $47 opcode
  EOR $83

; detect 6502 clone cores without (patented) Decimal arithmetic mode
  CMP #$4E ; ASCII 'N'
  BNE @naive
  CLI    ; avoid tripping up interrupt handlers with Decimal mode
  SED
  LDA #$90
  ADC #$10 ; in Decimal mode, result will be 0 or 1 with Carry set - in Binary mode, $A0 or $A1 with Carry clear
  CLD
  SEI
  LDA #$64 ; ASCII 'd'
  BCC @end
  LDA #$4E ; ASCII 'N'
; detect early NMOS 6502 without working ROR
; according to Michael Steil <https://www.pagetable.com/?p=406> faulty ROR behaves like ASL but doesn't update Carry
  ROL A
  ROR A
; with working ROR, Carry flag and accumulator would be unchanged
; with faulty ROR, accumulator is shifted left two places and Carry flag is cleared
  BCS @naive
  LDA #$4F ; ASCII 'O'

@naive:
; detect naive NMOS simulators
  CMP #$53 ; ASCII 'S'
  BNE @advanced
  BRA @advanced
  LDA #$6E ; ASCII 'n'

; detect 65CE02 and HuC6280
@advanced:
.P65816
  CMP #$43 ; ASCII 'C'
  BNE @end
  LDY #1 ; load Y with non-zero value
  CPY #1 ; set Zero flag
  REP #8 ; magic $C2 opcode: HuC6280 interprets as CLY,PHP - 65CE02 interprets as CPZ #8 - normal 65C02 interprets as NOP #imm
  BNE @ce02
  CPY #0
  BNE @end
  PLY
  LDA #$48 ; ASCII 'H'
  BRA @end
@ce02:
  LDA #$45 ; ASCII 'E'
@end:

; accumulator now contains ASCII:
; O - old NMOS 6502 (faulty ROR)
; N - real NMOS 6502
; n - NMOS simulator with 1-byte NOPs
; d - NMOS without Decimal mode (eg. Ricoh 2A03)
; S - 65SC02
; C - 65C02
; E - 65CE02
; H - HuC6280
; 8 - 65C816 or 65C802
This doesn't yet check for the Renesas 740. This check would probably have to be inserted at the top of the code.
Post Reply