Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
Martin Löffler | 074c1dd5ff | ||
Martin Löffler | 6955ca49ee | ||
Martin Löffler | a97824b26e | ||
Martin Löffler | 465aadd54e | ||
Martin Löffler | 01b87ade6d | ||
Martin Löffler | 8b86db359e | ||
Martin Löffler | 6d26e7de54 | ||
Martin Löffler | 8a1c99ee39 | ||
Martin Löffler | eb993eb1b4 | ||
Martin Löffler | 2fff8d2448 | ||
Martin Löffler | 6abdd5dff3 | ||
Martin Löffler | ed05dd5d30 | ||
Martin Löffler | 08a15a2e51 | ||
Martin Löffler | d8cf03afa1 | ||
Martin Löffler | cb26b0ff77 | ||
Martin Löffler | c61c814e55 | ||
Martin Löffler | c29389d3eb | ||
Martin Löffler | 51d2e29775 | ||
Martin Löffler | b9c5f33d93 | ||
Martin Löffler | d186950e99 | ||
Martin Löffler | c35340ecce | ||
Martin Löffler | 4017733dce | ||
Martin Löffler | 1b7b942dff | ||
Martin Löffler | 7737ab8de5 | ||
Martin Löffler | 17a8509898 |
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"rust-analyzer.checkOnSave.command": "clippy"
|
||||||
|
}
|
832
cpudiag.asm
Normal file
832
cpudiag.asm
Normal file
|
@ -0,0 +1,832 @@
|
||||||
|
.PROJECT cpudiag
|
||||||
|
|
||||||
|
ORG 0x0
|
||||||
|
JMP START ;JUMP TO 0x100
|
||||||
|
|
||||||
|
;***********************************************************************
|
||||||
|
; MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC VERSION 1.0 (C) 1980
|
||||||
|
;***********************************************************************
|
||||||
|
;
|
||||||
|
;DONATED TO THE "SIG/M" CP/M USER'S GROUP BY:
|
||||||
|
;KELLY SMITH, MICROCOSM ASSOCIATES
|
||||||
|
;3055 WACO AVENUE
|
||||||
|
;SIMI VALLEY, CALIFORNIA, 93065
|
||||||
|
;(805) 527-9321 (MODEM, CP/M-NET (TM))
|
||||||
|
;(805) 527-0518 (VERBAL)
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
|
||||||
|
ORG 00100H
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
START:
|
||||||
|
JMP CPU ;JUMP TO 8080 CPU DIAGNOSTIC
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
DB 'MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC'
|
||||||
|
DB ' VERSION 1.0 (C) 1980'
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;MESSAGE OUTPUT ROUTINE
|
||||||
|
;
|
||||||
|
MSG: PUSH D ;EXILE D REG.
|
||||||
|
PUSH A ;EXILE A REG.
|
||||||
|
XCHG ;SWAP H&L REGS. TO D&E REGS.
|
||||||
|
MSGLOOP:
|
||||||
|
LDAX D
|
||||||
|
CPI '$' ;TERMINATE IF WE ENCOUNTER A '$'
|
||||||
|
JZ MSGEND
|
||||||
|
|
||||||
|
OUT 0x1
|
||||||
|
INX D
|
||||||
|
JMP MSGLOOP
|
||||||
|
MSGEND:
|
||||||
|
POP A ;BACK FROM EXILE
|
||||||
|
POP D
|
||||||
|
RET
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;CHARACTER OUTPUT ROUTINE
|
||||||
|
;
|
||||||
|
PCHAR: PUSH A
|
||||||
|
MOV A, E
|
||||||
|
OUT 0x1
|
||||||
|
POP A
|
||||||
|
RET
|
||||||
|
|
||||||
|
TERMINATE:
|
||||||
|
MVI A, 0xa
|
||||||
|
OUT 0x1
|
||||||
|
OUT 0x1
|
||||||
|
DI
|
||||||
|
TERMLOOP:
|
||||||
|
HLT
|
||||||
|
JMP TERMLOOP
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
BYTEO: PUSH PSW
|
||||||
|
CALL BYTO1
|
||||||
|
MOV E,A
|
||||||
|
CALL PCHAR
|
||||||
|
POP PSW
|
||||||
|
CALL BYTO2
|
||||||
|
MOV E,A
|
||||||
|
JMP PCHAR
|
||||||
|
BYTO1: RRC
|
||||||
|
RRC
|
||||||
|
RRC
|
||||||
|
RRC
|
||||||
|
BYTO2: ANI 0FH
|
||||||
|
CPI 0AH
|
||||||
|
JM BYTO3
|
||||||
|
ADI 7
|
||||||
|
BYTO3: ADI 30H
|
||||||
|
RET
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;************************************************************
|
||||||
|
; MESSAGE TABLE FOR OPERATIONAL CPU TEST
|
||||||
|
;************************************************************
|
||||||
|
;
|
||||||
|
OKCPU: DB 0CH,0DH,0AH,' CPU IS OPERATIONAL$'
|
||||||
|
;
|
||||||
|
NGCPU: DB 0CH,0DH,0AH,' CPU HAS FAILED! ERROR EXIT=$'
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;************************************************************
|
||||||
|
; 8080/8085 CPU TEST/DIAGNOSTIC
|
||||||
|
;************************************************************
|
||||||
|
;
|
||||||
|
;NOTE: (1) PROGRAM ASSUMES "CALL",AND "LXI SP" INSTRUCTIONS WORK!
|
||||||
|
;
|
||||||
|
; (2) INSTRUCTIONS NOT TESTED ARE "HLT","DI","EI","RIM","SIM",
|
||||||
|
; AND "RST 0" THRU "RST 7"
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;TEST JUMP INSTRUCTIONS AND FLAGS
|
||||||
|
;
|
||||||
|
CPU: LXI SP,STACK ;SET THE STACK POINTER
|
||||||
|
ANI 0 ;INITIALIZE A REG. AND CLEAR ALL FLAGS
|
||||||
|
JZ J010 ;TEST "JZ"
|
||||||
|
CALL CPUER
|
||||||
|
J010: JNC J020 ;TEST "JNC"
|
||||||
|
CALL CPUER
|
||||||
|
J020: JPE J030 ;TEST "JPE"
|
||||||
|
CALL CPUER
|
||||||
|
J030: JP J040 ;TEST "JP"
|
||||||
|
CALL CPUER
|
||||||
|
J040: JNZ J050 ;TEST "JNZ"
|
||||||
|
JC J050 ;TEST "JC"
|
||||||
|
JPO J050 ;TEST "JPO"
|
||||||
|
JM J050 ;TEST "JM"
|
||||||
|
JMP J060 ;TEST "JMP" (IT'S A LITTLE LATE,BUT WHAT THE HELL!
|
||||||
|
J050: CALL CPUER
|
||||||
|
J060: ADI 6 ;A=6,C=0,P=1,S=0,Z=0
|
||||||
|
JNZ J070 ;TEST "JNZ"
|
||||||
|
CALL CPUER
|
||||||
|
J070: JC J080 ;TEST "JC"
|
||||||
|
JPO J080 ;TEST "JPO"
|
||||||
|
JP J090 ;TEST "JP"
|
||||||
|
J080: CALL CPUER
|
||||||
|
J090: ADI 070H ;A=76H,C=0,P=0,S=0,Z=0
|
||||||
|
JPO J100 ;TEST "JPO"
|
||||||
|
CALL CPUER
|
||||||
|
J100: JM J110 ;TEST "JM"
|
||||||
|
JZ J110 ;TEST "JZ"
|
||||||
|
JNC J120 ;TEST "JNC"
|
||||||
|
J110: CALL CPUER
|
||||||
|
J120: ADI 081H ;A=F7H,C=0,P=0,S=1,Z=0
|
||||||
|
JM J130 ;TEST "JM"
|
||||||
|
CALL CPUER
|
||||||
|
J130: JZ J140 ;TEST "JZ"
|
||||||
|
JC J140 ;TEST "JC"
|
||||||
|
JPO J150 ;TEST "JPO"
|
||||||
|
J140: CALL CPUER
|
||||||
|
J150: ADI 0FEH ;A=F5H,C=1,P=1,S=1,Z=0
|
||||||
|
JC J160 ;TEST "JC"
|
||||||
|
CALL CPUER
|
||||||
|
J160: JZ J170 ;TEST "JZ"
|
||||||
|
JPO J170 ;TEST "JPO"
|
||||||
|
JM AIMM ;TEST "JM"
|
||||||
|
J170: CALL CPUER
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;TEST ACCUMULATOR IMMEDIATE INSTRUCTIONS
|
||||||
|
;
|
||||||
|
AIMM: CPI 0 ;A=F5H,C=0,Z=0
|
||||||
|
JC CPIE ;TEST "CPI" FOR RE-SET CARRY
|
||||||
|
JZ CPIE ;TEST "CPI" FOR RE-SET ZERO
|
||||||
|
CPI 0F5H ;A=F5H,C=0,Z=1
|
||||||
|
JC CPIE ;TEST "CPI" FOR RE-SET CARRY ("ADI")
|
||||||
|
JNZ CPIE ;TEST "CPI" FOR RE-SET ZERO
|
||||||
|
CPI 0FFH ;A=F5H,C=1,Z=0
|
||||||
|
JZ CPIE ;TEST "CPI" FOR RE-SET ZERO
|
||||||
|
JC ACII ;TEST "CPI" FOR SET CARRY
|
||||||
|
CPIE: CALL CPUER
|
||||||
|
ACII: ACI 00AH ;A=F5H+0AH+CARRY(1)=0,C=1
|
||||||
|
ACI 00AH ;A=0+0AH+CARRY(0)=0BH,C=0
|
||||||
|
CPI 00BH
|
||||||
|
JZ SUII ;TEST "ACI"
|
||||||
|
CALL CPUER
|
||||||
|
SUII: SUI 00CH ;A=FFH,C=0
|
||||||
|
SUI 00FH ;A=F0H,C=1
|
||||||
|
CPI 0F0H
|
||||||
|
JZ SBII ;TEST "SUI"
|
||||||
|
CALL CPUER
|
||||||
|
SBII: SBI 0F1H ;A=F0H-0F1H-CARRY(0)=FFH,C=1
|
||||||
|
SBI 00EH ;A=FFH-OEH-CARRY(1)=F0H,C=0
|
||||||
|
CPI 0F0H
|
||||||
|
JZ ANII ;TEST "SBI"
|
||||||
|
CALL CPUER
|
||||||
|
ANII: ANI 055H ;A=F0H<AND>55H=50H,C=0,P=1,S=0,Z=0
|
||||||
|
CPI 050H
|
||||||
|
JZ ORII ;TEST "ANI"
|
||||||
|
CALL CPUER
|
||||||
|
ORII: ORI 03AH ;A=50H<OR>3AH=7AH,C=0,P=0,S=0,Z=0
|
||||||
|
CPI 07AH
|
||||||
|
JZ XRII ;TEST "ORI"
|
||||||
|
CALL CPUER
|
||||||
|
XRII: XRI 00FH ;A=7AH<XOR>0FH=75H,C=0,P=0,S=0,Z=0
|
||||||
|
CPI 075H
|
||||||
|
JZ C010 ;TEST "XRI"
|
||||||
|
CALL CPUER
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;TEST CALLS AND RETURNS
|
||||||
|
;
|
||||||
|
C010: ANI 000H ;A=0,C=0,P=1,S=0,Z=1
|
||||||
|
CC CPUER ;TEST "CC"
|
||||||
|
CPO CPUER ;TEST "CPO"
|
||||||
|
CM CPUER ;TEST "CM"
|
||||||
|
CNZ CPUER ;TEST "CNZ"
|
||||||
|
CPI 000H
|
||||||
|
JZ C020 ;A=0,C=0,P=0,S=0,Z=1
|
||||||
|
CALL CPUER
|
||||||
|
C020: SUI 077H ;A=89H,C=1,P=0,S=1,Z=0
|
||||||
|
CNC CPUER ;TEST "CNC"
|
||||||
|
CPE CPUER ;TEST "CPE"
|
||||||
|
CP CPUER ;TEST "CP"
|
||||||
|
CZ CPUER ;TEST "CZ"
|
||||||
|
CPI 089H
|
||||||
|
JZ C030 ;TEST FOR "CALLS" TAKING BRANCH
|
||||||
|
CALL CPUER
|
||||||
|
C030: ANI 0FFH ;SET FLAGS BACK!
|
||||||
|
CPO CPOI ;TEST "CPO"
|
||||||
|
CPI 0D9H
|
||||||
|
JZ MOVI ;TEST "CALL" SEQUENCE SUCCESS
|
||||||
|
CALL CPUER
|
||||||
|
CPOI: RPE ;TEST "RPE"
|
||||||
|
ADI 010H ;A=99H,C=0,P=0,S=1,Z=0
|
||||||
|
CPE CPEI ;TEST "CPE"
|
||||||
|
ADI 002H ;A=D9H,C=0,P=0,S=1,Z=0
|
||||||
|
RPO ;TEST "RPO"
|
||||||
|
CALL CPUER
|
||||||
|
CPEI: RPO ;TEST "RPO"
|
||||||
|
ADI 020H ;A=B9H,C=0,P=0,S=1,Z=0
|
||||||
|
CM CMI ;TEST "CM"
|
||||||
|
ADI 004H ;A=D7H,C=0,P=1,S=1,Z=0
|
||||||
|
RPE ;TEST "RPE"
|
||||||
|
CALL CPUER
|
||||||
|
CMI: RP ;TEST "RP"
|
||||||
|
ADI 080H ;A=39H,C=1,P=1,S=0,Z=0
|
||||||
|
CP TCPI ;TEST "CP"
|
||||||
|
ADI 080H ;A=D3H,C=0,P=0,S=1,Z=0
|
||||||
|
RM ;TEST "RM"
|
||||||
|
CALL CPUER
|
||||||
|
TCPI: RM ;TEST "RM"
|
||||||
|
ADI 040H ;A=79H,C=0,P=0,S=0,Z=0
|
||||||
|
CNC CNCI ;TEST "CNC"
|
||||||
|
ADI 040H ;A=53H,C=0,P=1,S=0,Z=0
|
||||||
|
RP ;TEST "RP"
|
||||||
|
CALL CPUER
|
||||||
|
CNCI: RC ;TEST "RC"
|
||||||
|
ADI 08FH ;A=08H,C=1,P=0,S=0,Z=0
|
||||||
|
CC CCI ;TEST "CC"
|
||||||
|
SUI 002H ;A=13H,C=0,P=0,S=0,Z=0
|
||||||
|
RNC ;TEST "RNC"
|
||||||
|
CALL CPUER
|
||||||
|
CCI: RNC ;TEST "RNC"
|
||||||
|
ADI 0F7H ;A=FFH,C=0,P=1,S=1,Z=0
|
||||||
|
CNZ CNZI ;TEST "CNZ"
|
||||||
|
ADI 0FEH ;A=15H,C=1,P=0,S=0,Z=0
|
||||||
|
RC ;TEST "RC"
|
||||||
|
CALL CPUER
|
||||||
|
CNZI: RZ ;TEST "RZ"
|
||||||
|
ADI 001H ;A=00H,C=1,P=1,S=0,Z=1
|
||||||
|
CZ CZI ;TEST "CZ"
|
||||||
|
ADI 0D0H ;A=17H,C=1,P=1,S=0,Z=0
|
||||||
|
RNZ ;TEST "RNZ"
|
||||||
|
CALL CPUER
|
||||||
|
CZI: RNZ ;TEST "RNZ"
|
||||||
|
ADI 047H ;A=47H,C=0,P=1,S=0,Z=0
|
||||||
|
CPI 047H ;A=47H,C=0,P=1,S=0,Z=1
|
||||||
|
RZ ;TEST "RZ"
|
||||||
|
CALL CPUER
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;TEST "MOV","INR",AND "DCR" INSTRUCTIONS
|
||||||
|
;
|
||||||
|
MOVI: MVI A,077H
|
||||||
|
INR A
|
||||||
|
MOV B,A
|
||||||
|
INR B
|
||||||
|
MOV C,B
|
||||||
|
DCR C
|
||||||
|
MOV D,C
|
||||||
|
MOV E,D
|
||||||
|
MOV H,E
|
||||||
|
MOV L,H
|
||||||
|
MOV A,L ;TEST "MOV" A,L,H,E,D,C,B,A
|
||||||
|
DCR A
|
||||||
|
MOV C,A
|
||||||
|
MOV E,C
|
||||||
|
MOV L,E
|
||||||
|
MOV B,L
|
||||||
|
MOV D,B
|
||||||
|
MOV H,D
|
||||||
|
MOV A,H ;TEST "MOV" A,H,D,B,L,E,C,A
|
||||||
|
MOV D,A
|
||||||
|
INR D
|
||||||
|
MOV L,D
|
||||||
|
MOV C,L
|
||||||
|
INR C
|
||||||
|
MOV H,C
|
||||||
|
MOV B,H
|
||||||
|
DCR B
|
||||||
|
MOV E,B
|
||||||
|
MOV A,E ;TEST "MOV" A,E,B,H,C,L,D,A
|
||||||
|
MOV E,A
|
||||||
|
INR E
|
||||||
|
MOV B,E
|
||||||
|
MOV H,B
|
||||||
|
INR H
|
||||||
|
MOV C,H
|
||||||
|
MOV L,C
|
||||||
|
MOV D,L
|
||||||
|
DCR D
|
||||||
|
MOV A,D ;TEST "MOV" A,D,L,C,H,B,E,A
|
||||||
|
MOV H,A
|
||||||
|
DCR H
|
||||||
|
MOV D,H
|
||||||
|
MOV B,D
|
||||||
|
MOV L,B
|
||||||
|
INR L
|
||||||
|
MOV E,L
|
||||||
|
DCR E
|
||||||
|
MOV C,E
|
||||||
|
MOV A,C ;TEST "MOV" A,C,E,L,B,D,H,A
|
||||||
|
MOV L,A
|
||||||
|
DCR L
|
||||||
|
MOV H,L
|
||||||
|
MOV E,H
|
||||||
|
MOV D,E
|
||||||
|
MOV C,D
|
||||||
|
MOV B,C
|
||||||
|
MOV A,B
|
||||||
|
CPI 077H
|
||||||
|
CNZ CPUER ;TEST "MOV" A,B,C,D,E,H,L,A
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;TEST ARITHMETIC AND LOGIC INSTRUCTIONS
|
||||||
|
;
|
||||||
|
XRA A
|
||||||
|
MVI B,001H
|
||||||
|
MVI C,003H
|
||||||
|
MVI D,007H
|
||||||
|
MVI E,00FH
|
||||||
|
MVI H,01FH
|
||||||
|
MVI L,03FH
|
||||||
|
ADD B
|
||||||
|
ADD C
|
||||||
|
ADD D
|
||||||
|
ADD E
|
||||||
|
ADD H
|
||||||
|
ADD L
|
||||||
|
ADD A
|
||||||
|
CPI 0F0H
|
||||||
|
CNZ CPUER ;TEST "ADD" B,C,D,E,H,L,A
|
||||||
|
SUB B
|
||||||
|
SUB C
|
||||||
|
SUB D
|
||||||
|
SUB E
|
||||||
|
SUB H
|
||||||
|
SUB L
|
||||||
|
CPI 078H
|
||||||
|
CNZ CPUER ;TEST "SUB" B,C,D,E,H,L
|
||||||
|
SUB A
|
||||||
|
CNZ CPUER ;TEST "SUB" A
|
||||||
|
MVI A,080H
|
||||||
|
ADD A
|
||||||
|
MVI B,001H
|
||||||
|
MVI C,002H
|
||||||
|
MVI D,003H
|
||||||
|
MVI E,004H
|
||||||
|
MVI H,005H
|
||||||
|
MVI L,006H
|
||||||
|
ADC B
|
||||||
|
MVI B,080H
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC C
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC D
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC E
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC H
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC L
|
||||||
|
ADD B
|
||||||
|
ADD B
|
||||||
|
ADC A
|
||||||
|
CPI 037H
|
||||||
|
CNZ CPUER ;TEST "ADC" B,C,D,E,H,L,A
|
||||||
|
MVI A,080H
|
||||||
|
ADD A
|
||||||
|
MVI B,001H
|
||||||
|
SBB B
|
||||||
|
MVI B,0FFH
|
||||||
|
ADD B
|
||||||
|
SBB C
|
||||||
|
ADD B
|
||||||
|
SBB D
|
||||||
|
ADD B
|
||||||
|
SBB E
|
||||||
|
ADD B
|
||||||
|
SBB H
|
||||||
|
ADD B
|
||||||
|
SBB L
|
||||||
|
CPI 0E0H
|
||||||
|
CNZ CPUER ;TEST "SBB" B,C,D,E,H,L
|
||||||
|
MVI A,080H
|
||||||
|
ADD A
|
||||||
|
SBB A
|
||||||
|
CPI 0FFH
|
||||||
|
CNZ CPUER ;TEST "SBB" A
|
||||||
|
MVI A,0FFH
|
||||||
|
MVI B,0FEH
|
||||||
|
MVI C,0FCH
|
||||||
|
MVI D,0EFH
|
||||||
|
MVI E,07FH
|
||||||
|
MVI H,0F4H
|
||||||
|
MVI L,0BFH
|
||||||
|
ANA A
|
||||||
|
ANA C
|
||||||
|
ANA D
|
||||||
|
ANA E
|
||||||
|
ANA H
|
||||||
|
ANA L
|
||||||
|
ANA A
|
||||||
|
CPI 024H
|
||||||
|
CNZ CPUER ;TEST "ANA" B,C,D,E,H,L,A
|
||||||
|
XRA A
|
||||||
|
MVI B,001H
|
||||||
|
MVI C,002H
|
||||||
|
MVI D,004H
|
||||||
|
MVI E,008H
|
||||||
|
MVI H,010H
|
||||||
|
MVI L,020H
|
||||||
|
ORA B
|
||||||
|
ORA C
|
||||||
|
ORA D
|
||||||
|
ORA E
|
||||||
|
ORA H
|
||||||
|
ORA L
|
||||||
|
ORA A
|
||||||
|
CPI 03FH
|
||||||
|
CNZ CPUER ;TEST "ORA" B,C,D,E,H,L,A
|
||||||
|
MVI A,000H
|
||||||
|
MVI H,08FH
|
||||||
|
MVI L,04FH
|
||||||
|
XRA B
|
||||||
|
XRA C
|
||||||
|
XRA D
|
||||||
|
XRA E
|
||||||
|
XRA H
|
||||||
|
XRA L
|
||||||
|
CPI 0CFH
|
||||||
|
CNZ CPUER ;TEST "XRA" B,C,D,E,H,L
|
||||||
|
XRA A
|
||||||
|
CNZ CPUER ;TEST "XRA" A
|
||||||
|
MVI B,044H
|
||||||
|
MVI C,045H
|
||||||
|
MVI D,046H
|
||||||
|
MVI E,047H
|
||||||
|
MVI H,(TEMP0 / 0FFH) ;HIGH BYTE OF TEST MEMORY LOCATION
|
||||||
|
MVI L,(TEMP0 & 0FFH) ;LOW BYTE OF TEST MEMORY LOCATION
|
||||||
|
MOV M,B
|
||||||
|
MVI B,000H
|
||||||
|
MOV B,M
|
||||||
|
MVI A,044H
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "MOV" M,B AND B,M
|
||||||
|
MOV M,D
|
||||||
|
MVI D,000H
|
||||||
|
MOV D,M
|
||||||
|
MVI A,046H
|
||||||
|
CMP D
|
||||||
|
CNZ CPUER ;TEST "MOV" M,D AND D,M
|
||||||
|
MOV M,E
|
||||||
|
MVI E,000H
|
||||||
|
MOV E,M
|
||||||
|
MVI A,047H
|
||||||
|
CMP E
|
||||||
|
CNZ CPUER ;TEST "MOV" M,E AND E,M
|
||||||
|
MOV M,H
|
||||||
|
MVI H,(TEMP0 / 0FFH)
|
||||||
|
MVI L,(TEMP0 & 0FFH)
|
||||||
|
MOV H,M
|
||||||
|
MVI A,(TEMP0 / 0FFH)
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "MOV" M,H AND H,M
|
||||||
|
MOV M,L
|
||||||
|
MVI H,(TEMP0 / 0FFH)
|
||||||
|
MVI L,(TEMP0 & 0FFH)
|
||||||
|
MOV L,M
|
||||||
|
MVI A,(TEMP0 & 0FFH)
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "MOV" M,L AND L,M
|
||||||
|
MVI H,(TEMP0 / 0FFH)
|
||||||
|
MVI L,(TEMP0 & 0FFH)
|
||||||
|
MVI A,032H
|
||||||
|
MOV M,A
|
||||||
|
CMP M
|
||||||
|
CNZ CPUER ;TEST "MOV" M,A
|
||||||
|
ADD M
|
||||||
|
CPI 064H
|
||||||
|
CNZ CPUER ;TEST "ADD" M
|
||||||
|
XRA A
|
||||||
|
MOV A,M
|
||||||
|
CPI 032H
|
||||||
|
CNZ CPUER ;TEST "MOV" A,M
|
||||||
|
MVI H,(TEMP0 / 0FFH)
|
||||||
|
MVI L,(TEMP0 & 0FFH)
|
||||||
|
MOV A,M
|
||||||
|
SUB M
|
||||||
|
CNZ CPUER ;TEST "SUB" M
|
||||||
|
MVI A,080H
|
||||||
|
ADD A
|
||||||
|
ADC M
|
||||||
|
CPI 033H
|
||||||
|
CNZ CPUER ;TEST "ADC" M
|
||||||
|
MVI A,080H
|
||||||
|
ADD A
|
||||||
|
SBB M
|
||||||
|
CPI 0CDH
|
||||||
|
CNZ CPUER ;TEST "SBB" M
|
||||||
|
ANA M
|
||||||
|
CNZ CPUER ;TEST "ANA" M
|
||||||
|
MVI A,025H
|
||||||
|
ORA M
|
||||||
|
CPI 037H
|
||||||
|
CNZ CPUER ;TEST "ORA" M
|
||||||
|
XRA M
|
||||||
|
CPI 005H
|
||||||
|
CNZ CPUER ;TEST "XRA" M
|
||||||
|
MVI M,055H
|
||||||
|
INR M
|
||||||
|
DCR M
|
||||||
|
ADD M
|
||||||
|
CPI 05AH
|
||||||
|
CNZ CPUER ;TEST "INR","DCR",AND "MVI" M
|
||||||
|
LXI B,12FFH
|
||||||
|
LXI D,12FFH
|
||||||
|
LXI H,12FFH
|
||||||
|
INX B
|
||||||
|
INX D
|
||||||
|
INX H
|
||||||
|
MVI A,013H
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" B
|
||||||
|
CMP D
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" D
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" H
|
||||||
|
MVI A,000H
|
||||||
|
CMP C
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" B
|
||||||
|
CMP E
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" D
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "LXI" AND "INX" H
|
||||||
|
DCX B
|
||||||
|
DCX D
|
||||||
|
DCX H
|
||||||
|
MVI A,012H
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "DCX" B
|
||||||
|
CMP D
|
||||||
|
CNZ CPUER ;TEST "DCX" D
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "DCX" H
|
||||||
|
MVI A,0FFH
|
||||||
|
CMP C
|
||||||
|
CNZ CPUER ;TEST "DCX" B
|
||||||
|
CMP E
|
||||||
|
CNZ CPUER ;TEST "DCX" D
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "DCX" H
|
||||||
|
STA TEMP0
|
||||||
|
XRA A
|
||||||
|
LDA TEMP0
|
||||||
|
CPI 0FFH
|
||||||
|
CNZ CPUER ;TEST "LDA" AND "STA"
|
||||||
|
LHLD TEMPP
|
||||||
|
SHLD TEMP0
|
||||||
|
LDA TEMPP
|
||||||
|
MOV B,A
|
||||||
|
LDA TEMP0
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "LHLD" AND "SHLD"
|
||||||
|
LDA TEMPP+1
|
||||||
|
MOV B,A
|
||||||
|
LDA TEMP0+1
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "LHLD" AND "SHLD"
|
||||||
|
MVI A,0AAH
|
||||||
|
STA TEMP0
|
||||||
|
MOV B,H
|
||||||
|
MOV C,L
|
||||||
|
XRA A
|
||||||
|
LDAX B
|
||||||
|
CPI 0AAH
|
||||||
|
CNZ CPUER ;TEST "LDAX" B
|
||||||
|
INR A
|
||||||
|
STAX B
|
||||||
|
LDA TEMP0
|
||||||
|
CPI 0ABH
|
||||||
|
CNZ CPUER ;TEST "STAX" B
|
||||||
|
MVI A,077H
|
||||||
|
STA TEMP0
|
||||||
|
LHLD TEMPP
|
||||||
|
LXI D,00000H
|
||||||
|
XCHG
|
||||||
|
XRA A
|
||||||
|
LDAX D
|
||||||
|
CPI 077H
|
||||||
|
CNZ CPUER ;TEST "LDAX" D AND "XCHG"
|
||||||
|
XRA A
|
||||||
|
ADD H
|
||||||
|
ADD L
|
||||||
|
CNZ CPUER ;TEST "XCHG"
|
||||||
|
MVI A,0CCH
|
||||||
|
STAX D
|
||||||
|
LDA TEMP0
|
||||||
|
CPI 0CCH
|
||||||
|
STAX D
|
||||||
|
LDA TEMP0
|
||||||
|
CPI 0CCH
|
||||||
|
CNZ CPUER ;TEST "STAX" D
|
||||||
|
LXI H,07777H
|
||||||
|
DAD H
|
||||||
|
MVI A,0EEH
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "DAD" H
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "DAD" H
|
||||||
|
LXI H,05555H
|
||||||
|
LXI B,0FFFFH
|
||||||
|
DAD B
|
||||||
|
MVI A,055H
|
||||||
|
CNC CPUER ;TEST "DAD" B
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "DAD" B
|
||||||
|
MVI A,054H
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "DAD" B
|
||||||
|
LXI H,0AAAAH
|
||||||
|
LXI D,03333H
|
||||||
|
DAD D
|
||||||
|
MVI A,0DDH
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "DAD" D
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "DAD" B
|
||||||
|
STC
|
||||||
|
CNC CPUER ;TEST "STC"
|
||||||
|
CMC
|
||||||
|
CC CPUER ;TEST "CMC
|
||||||
|
MVI A,0AAH
|
||||||
|
CMA
|
||||||
|
CPI 055H
|
||||||
|
CNZ CPUER ;TEST "CMA"
|
||||||
|
;ORA A ;RE-SET AUXILIARY CARRY
|
||||||
|
;DAA
|
||||||
|
;CPI 055H
|
||||||
|
;CNZ CPUER ;TEST "DAA"
|
||||||
|
;MVI A,088H
|
||||||
|
;ADD A
|
||||||
|
;DAA
|
||||||
|
;CPI 076H
|
||||||
|
;CNZ CPUER ;TEST "DAA"
|
||||||
|
;XRA A
|
||||||
|
;MVI A,0AAH
|
||||||
|
;DAA
|
||||||
|
;CNC CPUER ;TEST "DAA"
|
||||||
|
;CPI 010H
|
||||||
|
;CNZ CPUER ;TEST "DAA"
|
||||||
|
;XRA A
|
||||||
|
;MVI A,09AH
|
||||||
|
;DAA
|
||||||
|
;CNC CPUER ;TEST "DAA"
|
||||||
|
;CNZ CPUER ;TEST "DAA"
|
||||||
|
STC
|
||||||
|
MVI A,042H
|
||||||
|
RLC
|
||||||
|
CC CPUER ;TEST "RLC" FOR RE-SET CARRY
|
||||||
|
RLC
|
||||||
|
CNC CPUER ;TEST "RLC" FOR SET CARRY
|
||||||
|
CPI 009H
|
||||||
|
CNZ CPUER ;TEST "RLC" FOR ROTATION
|
||||||
|
RRC
|
||||||
|
CNC CPUER ;TEST "RRC" FOR SET CARRY
|
||||||
|
RRC
|
||||||
|
CPI 042H
|
||||||
|
CNZ CPUER ;TEST "RRC" FOR ROTATION
|
||||||
|
RAL
|
||||||
|
RAL
|
||||||
|
CNC CPUER ;TEST "RAL" FOR SET CARRY
|
||||||
|
CPI 008H
|
||||||
|
CNZ CPUER ;TEST "RAL" FOR ROTATION
|
||||||
|
RAR
|
||||||
|
RAR
|
||||||
|
CC CPUER ;TEST "RAR" FOR RE-SET CARRY
|
||||||
|
CPI 002H
|
||||||
|
CNZ CPUER ;TEST "RAR" FOR ROTATION
|
||||||
|
LXI B,01234H
|
||||||
|
LXI D,0AAAAH
|
||||||
|
LXI H,05555H
|
||||||
|
XRA A
|
||||||
|
PUSH B
|
||||||
|
PUSH D
|
||||||
|
PUSH H
|
||||||
|
PUSH PSW
|
||||||
|
LXI B,00000H
|
||||||
|
LXI D,00000H
|
||||||
|
LXI H,00000H
|
||||||
|
MVI A,0C0H
|
||||||
|
ADI 0F0H
|
||||||
|
POP PSW
|
||||||
|
POP H
|
||||||
|
POP D
|
||||||
|
POP B
|
||||||
|
CC CPUER ;TEST "PUSH PSW" AND "POP PSW"
|
||||||
|
CNZ CPUER ;TEST "PUSH PSW" AND "POP PSW"
|
||||||
|
CPO CPUER ;TEST "PUSH PSW" AND "POP PSW"
|
||||||
|
CM CPUER ;TEST "PUSH PSW" AND "POP PSW"
|
||||||
|
MVI A,012H
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "PUSH B" AND "POP B"
|
||||||
|
MVI A,034H
|
||||||
|
CMP C
|
||||||
|
CNZ CPUER ;TEST "PUSH B" AND "POP B"
|
||||||
|
MVI A,0AAH
|
||||||
|
CMP D
|
||||||
|
CNZ CPUER ;TEST "PUSH D" AND "POP D"
|
||||||
|
CMP E
|
||||||
|
CNZ CPUER ;TEST "PUSH D" AND "POP D"
|
||||||
|
MVI A,055H
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "PUSH H" AND "POP H"
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "PUSH H" AND "POP H"
|
||||||
|
LXI H,00000H
|
||||||
|
DAD SP
|
||||||
|
SHLD SAVSTK ;SAVE THE "OLD" STACK-POINTER!
|
||||||
|
LXI SP,TEMP4
|
||||||
|
DCX SP
|
||||||
|
DCX SP
|
||||||
|
INX SP
|
||||||
|
DCX SP
|
||||||
|
MVI A,055H
|
||||||
|
STA TEMP2
|
||||||
|
CMA
|
||||||
|
STA TEMP3
|
||||||
|
POP B
|
||||||
|
CMP B
|
||||||
|
CNZ CPUER ;TEST "LXI","DAD","INX",AND "DCX" SP
|
||||||
|
CMA
|
||||||
|
CMP C
|
||||||
|
CNZ CPUER ;TEST "LXI","DAD","INX", AND "DCX" SP
|
||||||
|
LXI H,TEMP4
|
||||||
|
SPHL
|
||||||
|
LXI H,07733H
|
||||||
|
DCX SP
|
||||||
|
DCX SP
|
||||||
|
XTHL
|
||||||
|
LDA TEMP3
|
||||||
|
CPI 077H
|
||||||
|
CNZ CPUER ;TEST "SPHL" AND "XTHL"
|
||||||
|
LDA TEMP2
|
||||||
|
CPI 033H
|
||||||
|
CNZ CPUER ;TEST "SPHL" AND "XTHL"
|
||||||
|
MVI A,055H
|
||||||
|
CMP L
|
||||||
|
CNZ CPUER ;TEST "SPHL" AND "XTHL"
|
||||||
|
CMA
|
||||||
|
CMP H
|
||||||
|
CNZ CPUER ;TEST "SPHL" AND "XTHL"
|
||||||
|
LHLD SAVSTK ;RESTORE THE "OLD" STACK-POINTER
|
||||||
|
SPHL
|
||||||
|
LXI H,CPUOK
|
||||||
|
PCHL ;TEST "PCHL"
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
CPUER: LXI H,NGCPU ;OUTPUT "CPU HAS FAILED ERROR EXIT=" TO CONSOLE
|
||||||
|
CALL MSG
|
||||||
|
XTHL
|
||||||
|
MOV A,H
|
||||||
|
CALL BYTEO ;SHOW ERROR EXIT ADDRESS HIGH BYTE
|
||||||
|
MOV A,L
|
||||||
|
CALL BYTEO ;SHOW ERROR EXIT ADDRESS LOW BYTE
|
||||||
|
JMP TERMINATE
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
CPUOK: LXI H,OKCPU ;OUTPUT "CPU IS OPERATIONAL" TO CONSOLE
|
||||||
|
CALL MSG
|
||||||
|
JMP TERMINATE
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
TEMPP: DW TEMP0 ;POINTER USED TO TEST "LHLD","SHLD",
|
||||||
|
; AND "LDAX" INSTRUCTIONS
|
||||||
|
;
|
||||||
|
TEMP0: DS 1 ;TEMPORARY STORAGE FOR CPU TEST MEMORY LOCATIONS
|
||||||
|
TEMP1: DS 1 ;TEMPORARY STORAGE FOR CPU TEST MEMORY LOCATIONS
|
||||||
|
TEMP2 DS 1 ;TEMPORARY STORAGE FOR CPU TEST MEMORY LOCATIONS
|
||||||
|
TEMP3: DS 1 ;TEMPORARY STORAGE FOR CPU TEST MEMORY LOCATIONS
|
||||||
|
TEMP4: DS 1 ;TEMPORARY STORAGE FOR CPU TEST MEMORY LOCATIONS
|
||||||
|
SAVSTK: DS 2 ;TEMPORARY STACK-POINTER STORAGE LOCATION
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
STACK EQU TEMPP+256 ;DE-BUG STACK POINTER STORAGE AREA
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
END
|
BIN
cpudiag.bin
Normal file
BIN
cpudiag.bin
Normal file
Binary file not shown.
|
@ -15,7 +15,7 @@ fn main() {
|
||||||
let mut index: u32 = 0;
|
let mut index: u32 = 0;
|
||||||
|
|
||||||
while let Some(byte) = data.next() {
|
while let Some(byte) = data.next() {
|
||||||
let current_index = index.clone();
|
let current_index = index;
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
let mut next = |len: u8| {
|
let mut next = |len: u8| {
|
||||||
|
|
|
@ -1,69 +1,133 @@
|
||||||
use crate::{EmulatorState, Register, get_register, structs::set_register};
|
use crate::mapper::MemoryMapper;
|
||||||
|
use crate::structs::{get_register_pair, set_register, set_register_pair};
|
||||||
|
use crate::{get_register, EmulatorState, Register};
|
||||||
|
|
||||||
/// Sets the condition code flags according to `result`. `flags` parameter
|
/// Sets the condition code flags according to `result`.
|
||||||
/// indicates which flags will be set, 0b1111 will set all (Z, S, C, P)
|
/// Does not set the carry flag and will always set the Z, S and P flags.
|
||||||
/// while 0b1000 will only set Z.
|
#[inline(always)]
|
||||||
fn set_cc(state: &mut EmulatorState, result: u16, flags: u8) {
|
fn set_cc<M: MemoryMapper>(state: &mut EmulatorState<M>, result: u8) {
|
||||||
if flags & 0b1000 > 0 { state.cc.z = (result & 0xff) == 0; }
|
state.cc.z = result == 0;
|
||||||
if flags & 0b0100 > 0 { state.cc.s = (result & 0x80) > 0; }
|
state.cc.s = result & 0x80 > 0;
|
||||||
if flags & 0b0010 > 0 { state.cc.c = result > 0xff; }
|
state.cc.p = result.count_ones() % 2 == 0;
|
||||||
if flags & 0b0001 > 0 { state.cc.p = (result & 0xff).count_ones() % 2 == 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add values of `register` and `A`
|
/// Add values of `register` and `A`, add +1 if carry arg is set (either false or state.cc.c)
|
||||||
pub fn add(register: Register, state: &mut EmulatorState) {
|
pub fn add_reg<M: MemoryMapper>(register: Register, carry: bool, state: &mut EmulatorState<M>) {
|
||||||
let result = get_register(®ister, state) as u16 + state.a as u16;
|
add(get_register(register, state), carry, state);
|
||||||
set_cc(state, result, 0b1111);
|
|
||||||
state.a = (result & 0xff) as u8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add values of input byte and `A`
|
/// Add values of input byte and `A`, add +1 if carry arg is set (either false or state.cc.c)
|
||||||
pub fn adi(byte: u8, state: &mut EmulatorState) {
|
pub fn add<M: MemoryMapper>(byte: u8, carry: bool, state: &mut EmulatorState<M>) {
|
||||||
let result = state.a as u16 + byte as u16;
|
let (a, first) = state.a.overflowing_add(byte);
|
||||||
set_cc(state, result, 0b1111);
|
let (result, second) = a.overflowing_add(carry as u8);
|
||||||
state.a = result as u8;
|
|
||||||
|
state.cc.c = first != second;
|
||||||
|
set_cc(state, result);
|
||||||
|
state.a = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add values of `register` and `A` and add +1 if carry bit is set
|
pub fn sub_reg<M: MemoryMapper>(register: Register, borrow: bool, state: &mut EmulatorState<M>) {
|
||||||
pub fn adc(register: Register, state: &mut EmulatorState) {
|
sub(get_register(register, state), borrow, state);
|
||||||
let result = get_register(®ister, state) as u16 + state.a as u16 + if state.cc.c { 1 } else { 0 };
|
|
||||||
set_cc(state, result, 0b1111);
|
|
||||||
state.a = (result & 0xff) as u8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add values of input byte and `A` and add +1 if carry bit is set
|
pub fn sub<M: MemoryMapper>(byte: u8, borrow: bool, state: &mut EmulatorState<M>) {
|
||||||
pub fn aci(byte: u8, state: &mut EmulatorState) {
|
let (a, first) = state.a.overflowing_sub(byte);
|
||||||
let result = state.a as u16 + byte as u16 + if state.cc.c { 1 } else { 0 };
|
let (result, second) = a.overflowing_sub(borrow as u8);
|
||||||
set_cc(state, result, 0b1111);
|
|
||||||
state.a = result as u8;
|
state.cc.c = first != second;
|
||||||
|
set_cc(state, result);
|
||||||
|
state.a = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! bitwise_op {
|
||||||
|
($name:ident, $reg_name:ident, $op:tt) => {
|
||||||
|
pub fn $reg_name<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
$name(get_register(register, state), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn $name<M: MemoryMapper>(byte: u8, state: &mut EmulatorState<M>) {
|
||||||
|
let result = state.a $op byte;
|
||||||
|
state.cc.c = false;
|
||||||
|
set_cc(state, result);
|
||||||
|
state.a = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitwise_op!(and, and_reg, &);
|
||||||
|
bitwise_op!(or, or_reg, |);
|
||||||
|
bitwise_op!(xor, xor_reg, ^);
|
||||||
|
|
||||||
|
pub fn cmp_reg<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
cmp(get_register(register, state), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmp<M: MemoryMapper>(byte: u8, state: &mut EmulatorState<M>) {
|
||||||
|
let (result, carry) = state.a.overflowing_sub(byte);
|
||||||
|
state.cc.c = carry;
|
||||||
|
set_cc(state, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Double precision add - Add B&C, D&E or H&L to H&L
|
/// Double precision add - Add B&C, D&E or H&L to H&L
|
||||||
pub fn dad(register: Register, state: &mut EmulatorState) {
|
pub fn dad<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
let num = match register {
|
let num = get_register_pair(register, state);
|
||||||
Register::B => u16::from_le_bytes([state.c, state.b]),
|
let (result, overflow) = num.overflowing_add(get_register_pair(Register::H, state));
|
||||||
Register::D => u16::from_le_bytes([state.e, state.d]),
|
|
||||||
Register::H => u16::from_le_bytes([state.l, state.h]),
|
|
||||||
Register::SP => state.sp,
|
|
||||||
_ => panic!("Cannot perform DAD on register {:?}", register),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (result, overflow) = num.overflowing_add(u16::from_le_bytes([state.l, state.h]));
|
// this is the only 16-bit arithmetic function that sets the other flags
|
||||||
|
state.cc.z = result == 0;
|
||||||
|
state.cc.s = result & 0x8000 > 0;
|
||||||
state.cc.c = overflow;
|
state.cc.c = overflow;
|
||||||
state.h = (result >> 8) as u8;
|
state.cc.p = result.count_ones() % 2 == 0;
|
||||||
state.l = result as u8;
|
set_register_pair(Register::H, result, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increase register
|
/// Increase register
|
||||||
pub fn inr(register: Register, state: &mut EmulatorState) {
|
pub fn inr<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
let (result, _) = get_register(®ister, state).overflowing_add(1);
|
let (result, _) = get_register(register, state).overflowing_add(1);
|
||||||
set_cc(state, result as u16, 0b1101);
|
set_cc(state, result);
|
||||||
set_register(®ister, result, state);
|
set_register(register, result, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrease register
|
/// Decrease register
|
||||||
pub fn dcr(register: Register, state: &mut EmulatorState) {
|
pub fn dcr<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
let (result, _) = get_register(®ister, state).overflowing_sub(1);
|
let (result, _) = get_register(register, state).overflowing_sub(1);
|
||||||
set_cc(state, result as u16, 0b1101);
|
set_cc(state, result);
|
||||||
set_register(®ister, result, state);
|
set_register(register, result, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increase register pair
|
||||||
|
pub fn inx<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
let (result, _) = get_register_pair(register, state).overflowing_add(1);
|
||||||
|
set_register_pair(register, result, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrease register pair
|
||||||
|
pub fn dcx<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
let (result, _) = get_register_pair(register, state).overflowing_sub(1);
|
||||||
|
set_register_pair(register, result, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rlc<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
let result = state.a.rotate_left(1);
|
||||||
|
state.a = result;
|
||||||
|
state.cc.c = result & 0x01 > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rrc<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
state.cc.c = state.a & 0x01 > 0;
|
||||||
|
state.a = state.a.rotate_right(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ral<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
let new_carry = state.a & 0x80 > 0;
|
||||||
|
let result = state.a << 1;
|
||||||
|
state.a = result | state.cc.c as u8;
|
||||||
|
state.cc.c = new_carry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rar<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
let new_carry = state.a & 0x01 > 0;
|
||||||
|
let result = state.a >> 1;
|
||||||
|
state.a = result | (state.cc.c as u8) << 7;
|
||||||
|
state.cc.c = new_carry;
|
||||||
}
|
}
|
||||||
|
|
53
src/emulator/instructions/branch.rs
Normal file
53
src/emulator/instructions/branch.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use crate::mapper::MemoryMapper;
|
||||||
|
use crate::transfer::{pop, push};
|
||||||
|
use crate::{get_register_pair, EmulatorState, Register};
|
||||||
|
|
||||||
|
/// Jump (set PC) to specified address
|
||||||
|
pub fn jump<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
state.pc = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jump to a specified address only if `cond` is true
|
||||||
|
pub fn jump_cond<M: MemoryMapper>(address: u16, cond: bool, state: &mut EmulatorState<M>) {
|
||||||
|
if cond {
|
||||||
|
jump(address, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the current PC to the stack and jump to the specified address
|
||||||
|
pub fn call<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
push(state.pc, state);
|
||||||
|
jump(address, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the current PC to the stack and jump to the specified address if `cond` is true
|
||||||
|
pub fn call_cond<M: MemoryMapper>(address: u16, cond: bool, state: &mut EmulatorState<M>) {
|
||||||
|
if cond {
|
||||||
|
call(address, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop a value from the stack and jump to it
|
||||||
|
pub fn ret<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
jump(pop(state), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop a value from the stack and jump to it if `cond` is true
|
||||||
|
pub fn ret_cond<M: MemoryMapper>(cond: bool, state: &mut EmulatorState<M>) {
|
||||||
|
if cond {
|
||||||
|
ret(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Restart" at (call) a specific interrupt vector
|
||||||
|
///
|
||||||
|
/// Panics if `vector` is 8 or above
|
||||||
|
pub fn restart<M: MemoryMapper>(vector: u8, state: &mut EmulatorState<M>) {
|
||||||
|
assert!(vector < 8, "Illegal restart vector");
|
||||||
|
call((vector * 8) as u16, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a new program counter from the HL register pair
|
||||||
|
pub fn pchl<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
state.pc = get_register_pair(Register::H, state);
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
pub mod arithmetic;
|
pub mod arithmetic;
|
||||||
|
pub mod branch;
|
||||||
|
pub mod transfer;
|
||||||
|
|
145
src/emulator/instructions/transfer.rs
Normal file
145
src/emulator/instructions/transfer.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::mapper::MemoryMapper;
|
||||||
|
use crate::{
|
||||||
|
get_register, get_register_pair, set_register, set_register_pair, EmulatorState, Register,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Move (copy) value from source to destination register
|
||||||
|
pub fn mov<M: MemoryMapper>(src: Register, dest: Register, state: &mut EmulatorState<M>) {
|
||||||
|
let data = get_register(src, state);
|
||||||
|
set_register(dest, data, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move immediate into destination register
|
||||||
|
pub fn mvi<M: MemoryMapper>(byte: u8, dest: Register, state: &mut EmulatorState<M>) {
|
||||||
|
set_register(dest, byte, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store accumulator to the speficied address
|
||||||
|
pub fn sta<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
state.write_byte(address, state.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load accumulator from the specified address
|
||||||
|
pub fn lda<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
state.a = state.read_byte(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store accumulator using the BC or DE register pair
|
||||||
|
pub fn stax<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
let address = get_register_pair(register, state);
|
||||||
|
state.write_byte(address, state.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load accumulator using the BC or DE register pair
|
||||||
|
pub fn ldax<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
let address = get_register_pair(register, state);
|
||||||
|
state.a = state.read_byte(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a 16-bit word from H and L to the specified address
|
||||||
|
pub fn shld<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
state.write_byte(address, state.l);
|
||||||
|
state.write_byte(address.overflowing_add(1).0, state.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a 16-bit word into H and L from the specified address
|
||||||
|
pub fn lhld<M: MemoryMapper>(address: u16, state: &mut EmulatorState<M>) {
|
||||||
|
state.l = state.read_byte(address);
|
||||||
|
state.h = state.read_byte(address.overflowing_add(1).0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exchange the HL register pair with the value at the top of the stack
|
||||||
|
pub fn xthl<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
let at_hl = get_register_pair(Register::H, state);
|
||||||
|
let at_stack = state.read_word(state.sp);
|
||||||
|
// Set HL to the 16-bit value currently on top of the stack
|
||||||
|
set_register_pair(Register::H, at_stack, state);
|
||||||
|
// Set the 16-bit word at the current stack position to what HL was
|
||||||
|
state.write_word(state.sp, at_hl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the stack pointer from the HL register pair
|
||||||
|
pub fn sphl<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
state.sp = get_register_pair(Register::H, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exchange the HL and DE register pairs
|
||||||
|
pub fn xchg<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
|
let at_hl = get_register_pair(Register::H, state);
|
||||||
|
let at_de = get_register_pair(Register::D, state);
|
||||||
|
// Set HL to DE
|
||||||
|
set_register_pair(Register::H, at_de, state);
|
||||||
|
// Set DE to previous value of HL
|
||||||
|
set_register_pair(Register::D, at_hl, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STACK */
|
||||||
|
/// Push a 16-bit value from a register pair onto the stack
|
||||||
|
pub fn push_reg<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
push(get_register_pair(register, state), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a 16-bit value onto the stack
|
||||||
|
pub fn push<M: MemoryMapper>(value: u16, state: &mut EmulatorState<M>) {
|
||||||
|
(state.sp, ..) = state.sp.overflowing_sub(2);
|
||||||
|
state.write_word(state.sp, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop a 16-bit value from the stack into a register pair
|
||||||
|
pub fn pop_reg<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) {
|
||||||
|
set_register_pair(register, pop(state), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop a 16-bit value from the stack
|
||||||
|
pub fn pop<M: MemoryMapper>(state: &mut EmulatorState<M>) -> u16 {
|
||||||
|
let value = state.read_word(state.sp);
|
||||||
|
(state.sp, ..) = state.sp.overflowing_add(2);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::mapper::TestMapper;
|
||||||
|
use crate::EmulatorState;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push() {
|
||||||
|
// From the altairclone.com 8080 programmers manual:
|
||||||
|
// (1) The most significant 8 bits of data are stored at the
|
||||||
|
// memory address one less than the contents of the
|
||||||
|
// stack pointer.
|
||||||
|
// (2) The least significant 8 bits of data are stored at the
|
||||||
|
// memory address two less than the contents of the
|
||||||
|
// stack pointer.
|
||||||
|
// (3) The stack pointer is automatically decremented by two.
|
||||||
|
let mut state: EmulatorState<TestMapper> = EmulatorState {
|
||||||
|
sp: 0x12,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
super::push(0x1234, &mut state);
|
||||||
|
assert_eq!(state.sp, 0x10);
|
||||||
|
assert_eq!(state.mapper.0[0x10..=0x11], [0x34, 0x12]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pop() {
|
||||||
|
// From the altairclone.com 8080 programmers manual:
|
||||||
|
// (1) The second register of the pair, or the least significant
|
||||||
|
// 8 bits of the program counter, are loaded from the
|
||||||
|
// memory address held in the stack pointer.
|
||||||
|
// (2) The first register of the pair, or the most significant
|
||||||
|
// 8 bits of the program counter, are loaded from the
|
||||||
|
// memory address one greater than the address held in
|
||||||
|
// the stack pointer.
|
||||||
|
// (3) The stack pointer is automatically incremented by two.
|
||||||
|
let mut state: EmulatorState<TestMapper> = EmulatorState {
|
||||||
|
sp: 0x10,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
state.mapper.0[0x10] = 0x34;
|
||||||
|
state.mapper.0[0x11] = 0x12;
|
||||||
|
assert_eq!(super::pop(&mut state), 0x1234);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,18 @@
|
||||||
use std::{fs, env};
|
use instructions::{arithmetic, branch, transfer};
|
||||||
use instructions::arithmetic;
|
use mapper::MemoryMapper;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
use crate::structs::*;
|
use crate::structs::*;
|
||||||
|
|
||||||
mod instructions;
|
mod instructions;
|
||||||
|
mod mapper;
|
||||||
mod structs;
|
mod structs;
|
||||||
|
|
||||||
pub const MEMORY_SIZE: usize = 8192;
|
pub const MEMORY_SIZE: usize = 8192;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut state = EmulatorState {
|
use crate::mapper::TestMapper;
|
||||||
a: 0,
|
let mut state = EmulatorState::<TestMapper>::default();
|
||||||
b: 0,
|
|
||||||
c: 0,
|
|
||||||
d: 0,
|
|
||||||
e: 0,
|
|
||||||
h: 0,
|
|
||||||
l: 0,
|
|
||||||
sp: 0,
|
|
||||||
cc: ConditionCodes { z: true, s: true, p: true, c: true },
|
|
||||||
pc: 0,
|
|
||||||
ei: true,
|
|
||||||
memory: [0; MEMORY_SIZE],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load the ROM into memory
|
// Load the ROM into memory
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
|
@ -31,11 +21,10 @@ fn main() {
|
||||||
.expect("Provide a path to a ROM file to emulate as an argument");
|
.expect("Provide a path to a ROM file to emulate as an argument");
|
||||||
let file = fs::read(filename).expect("where file");
|
let file = fs::read(filename).expect("where file");
|
||||||
|
|
||||||
for i in 0..(MEMORY_SIZE.min(file.len())) {
|
let to_copy = state.mapper.0.len().min(file.len());
|
||||||
state.memory[i] = file[i];
|
state.mapper.0[..to_copy].copy_from_slice(&file[..to_copy]);
|
||||||
}
|
|
||||||
|
|
||||||
while state.pc < MEMORY_SIZE as u16 {
|
while state.pc < u16::MAX {
|
||||||
tick(&mut state);
|
tick(&mut state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,19 +32,82 @@ fn main() {
|
||||||
print_state(&state);
|
print_state(&state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick(state: &mut EmulatorState) {
|
fn tick<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
let instruction = state.memory[state.pc as usize];
|
let instruction = state.next_byte();
|
||||||
|
|
||||||
let mut next_byte = || {
|
|
||||||
state.pc += 1;
|
|
||||||
return state.memory[state.pc as usize];
|
|
||||||
};
|
|
||||||
|
|
||||||
match instruction {
|
match instruction {
|
||||||
0x00 => {} // NOP
|
0x00 => {} // NOP
|
||||||
|
|
||||||
/* Maths */
|
/* Special */
|
||||||
|
0xd3 => {
|
||||||
|
// OUT
|
||||||
|
let port = state.next_byte();
|
||||||
|
state.write_io(port, state.a);
|
||||||
|
}
|
||||||
|
0xdb => {
|
||||||
|
// IN
|
||||||
|
let port = state.next_byte();
|
||||||
|
state.a = state.read_io(port);
|
||||||
|
}
|
||||||
|
0xfb => state.ei = true, // EI
|
||||||
|
0xf3 => state.ei = false, // DI
|
||||||
|
0x76 => {
|
||||||
|
// HLT
|
||||||
|
if state.ei {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
println!("HLT called after DI; exiting.");
|
||||||
|
print_state(state);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Transfer */
|
||||||
|
// MVI
|
||||||
|
0x06 | 0x0e | 0x16 | 0x1e | 0x26 | 0x2e | 0x36 | 0x3e => transfer::mvi(
|
||||||
|
state.next_byte(),
|
||||||
|
register_from_num((instruction & 0x38) >> 3),
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
|
||||||
|
// MOV
|
||||||
|
0x40..=0x7f => transfer::mov(
|
||||||
|
register_from_num(instruction & 0x7),
|
||||||
|
register_from_num((instruction & 0x38) >> 3),
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Accumulator transfer instructions
|
||||||
|
0x02 => transfer::stax(Register::B, state), // STAX B
|
||||||
|
0x0a => transfer::ldax(Register::B, state), // LDAX B
|
||||||
|
0x12 => transfer::stax(Register::D, state), // STAX D
|
||||||
|
0x1a => transfer::ldax(Register::D, state), // LDAX D
|
||||||
|
0x32 => transfer::sta(state.next_word(), state), // STA
|
||||||
|
0x3a => transfer::lda(state.next_word(), state), // LDA
|
||||||
|
|
||||||
|
// 16-bit transfer instructions
|
||||||
|
0x01 => set_register_pair(Register::B, state.next_word(), state), // LXI B
|
||||||
|
0x11 => set_register_pair(Register::D, state.next_word(), state), // LXI D
|
||||||
|
0x21 => set_register_pair(Register::H, state.next_word(), state), // LXI H
|
||||||
|
0x31 => set_register_pair(Register::SP, state.next_word(), state), // LXI SP
|
||||||
|
0x22 => transfer::shld(state.next_word(), state), // SHLD
|
||||||
|
0x2a => transfer::lhld(state.next_word(), state), // LHLD
|
||||||
|
|
||||||
|
0xe3 => transfer::xthl(state),
|
||||||
|
0xeb => transfer::xchg(state),
|
||||||
|
0xf9 => transfer::sphl(state),
|
||||||
|
|
||||||
|
// Stack instructions
|
||||||
|
0xc1 => transfer::pop_reg(Register::B, state), // POP B
|
||||||
|
0xc5 => transfer::push_reg(Register::B, state), // PUSH B
|
||||||
|
0xd1 => transfer::pop_reg(Register::D, state), // POP D
|
||||||
|
0xd5 => transfer::push_reg(Register::D, state), // PUSH D
|
||||||
|
0xe1 => transfer::pop_reg(Register::H, state), // POP H
|
||||||
|
0xe5 => transfer::push_reg(Register::H, state), // PUSH H
|
||||||
|
0xf1 => transfer::pop_reg(Register::A, state), // POP PSW
|
||||||
|
0xf5 => transfer::push_reg(Register::A, state), // PUSH PSW
|
||||||
|
|
||||||
|
/* Maths */
|
||||||
// INR
|
// INR
|
||||||
0x04 => arithmetic::inr(Register::B, state),
|
0x04 => arithmetic::inr(Register::B, state),
|
||||||
0x0c => arithmetic::inr(Register::C, state),
|
0x0c => arithmetic::inr(Register::C, state),
|
||||||
|
@ -82,28 +134,104 @@ fn tick(state: &mut EmulatorState) {
|
||||||
0x29 => arithmetic::dad(Register::H, state),
|
0x29 => arithmetic::dad(Register::H, state),
|
||||||
0x39 => arithmetic::dad(Register::SP, state),
|
0x39 => arithmetic::dad(Register::SP, state),
|
||||||
|
|
||||||
0x80..=0x87 => arithmetic::add(register_from_num(instruction & 0xf), state), // ADD
|
// INX
|
||||||
0x88..=0x8f => arithmetic::adc(register_from_num(instruction & 0xf), state), // ADC
|
0x03 => arithmetic::inx(Register::B, state),
|
||||||
0xc6 => arithmetic::adi(next_byte(), state), // ADI
|
0x13 => arithmetic::inx(Register::D, state),
|
||||||
0xce => arithmetic::aci(next_byte(), state), // ACI
|
0x23 => arithmetic::inx(Register::H, state),
|
||||||
|
0x33 => arithmetic::inx(Register::SP, state),
|
||||||
|
|
||||||
|
// DCX
|
||||||
|
0x0b => arithmetic::dcx(Register::B, state),
|
||||||
|
0x1b => arithmetic::dcx(Register::D, state),
|
||||||
|
0x2b => arithmetic::dcx(Register::H, state),
|
||||||
|
0x3b => arithmetic::dcx(Register::SP, state),
|
||||||
|
|
||||||
/* Special */
|
// Accumulator rotates
|
||||||
0xfb => state.ei = true, // EI
|
0x07 => arithmetic::rlc(state),
|
||||||
0xf3 => state.ei = false, // DI
|
0x0f => arithmetic::rrc(state),
|
||||||
0x76 => if state.ei { todo!() } else { // HLT
|
0x17 => arithmetic::ral(state),
|
||||||
println!("HLT called after DI; exiting.");
|
0x1f => arithmetic::rar(state),
|
||||||
print_state(state);
|
|
||||||
std::process::exit(0);
|
// Carry and complements
|
||||||
},
|
0x27 => panic!("Auxiliary Carry not implemented, unable to execute DAA instruction"),
|
||||||
|
0x2f => state.a = !state.a, // CMA
|
||||||
|
0x37 => state.cc.c = true, // STC
|
||||||
|
0x3f => state.cc.c = !state.cc.c, // CMC
|
||||||
|
|
||||||
|
0x80..=0x87 => arithmetic::add_reg(register_from_num(instruction & 0x7), false, state), // ADD
|
||||||
|
0x88..=0x8f => arithmetic::add_reg(register_from_num(instruction & 0x7), state.cc.c, state), // ADC
|
||||||
|
0xc6 => arithmetic::add(state.next_byte(), false, state), // ADI
|
||||||
|
0xce => arithmetic::add(state.next_byte(), state.cc.c, state), // ACI
|
||||||
|
|
||||||
|
0x90..=0x97 => arithmetic::sub_reg(register_from_num(instruction & 0x7), false, state), // SUB
|
||||||
|
0x98..=0x9f => arithmetic::sub_reg(register_from_num(instruction & 0x7), state.cc.c, state), // SBB
|
||||||
|
0xd6 => arithmetic::sub(state.next_byte(), false, state), // SUI
|
||||||
|
0xde => arithmetic::sub(state.next_byte(), state.cc.c, state), // SBI
|
||||||
|
|
||||||
|
0xa0..=0xa7 => arithmetic::and_reg(register_from_num(instruction & 0x7), state), // ANA
|
||||||
|
0xa8..=0xaf => arithmetic::xor_reg(register_from_num(instruction & 0x7), state), // XRA
|
||||||
|
0xb0..=0xb7 => arithmetic::or_reg(register_from_num(instruction & 0x7), state), // ORA
|
||||||
|
0xb8..=0xbf => arithmetic::cmp_reg(register_from_num(instruction & 0x7), state), // CMP
|
||||||
|
0xe6 => arithmetic::and(state.next_byte(), state), // ANI
|
||||||
|
0xee => arithmetic::xor(state.next_byte(), state), // XRI
|
||||||
|
0xf6 => arithmetic::or(state.next_byte(), state), // ORI
|
||||||
|
0xfe => arithmetic::cmp(state.next_byte(), state), // CPI
|
||||||
|
|
||||||
|
/* Branch instructions */
|
||||||
|
// Jumps
|
||||||
|
0xc2 => branch::jump_cond(state.next_word(), !state.cc.z, state), // JNZ
|
||||||
|
0xc3 => branch::jump(state.next_word(), state), // JMP
|
||||||
|
0xca => branch::jump_cond(state.next_word(), state.cc.z, state), // JZ
|
||||||
|
0xd2 => branch::jump_cond(state.next_word(), !state.cc.c, state), // JNC
|
||||||
|
0xda => branch::jump_cond(state.next_word(), state.cc.c, state), // JC
|
||||||
|
0xe2 => branch::jump_cond(state.next_word(), !state.cc.p, state), // JPO
|
||||||
|
0xea => branch::jump_cond(state.next_word(), state.cc.p, state), // JPE
|
||||||
|
0xf2 => branch::jump_cond(state.next_word(), !state.cc.s, state), // JP
|
||||||
|
0xfa => branch::jump_cond(state.next_word(), state.cc.s, state), // JM
|
||||||
|
|
||||||
|
// Calls
|
||||||
|
0xc4 => branch::call_cond(state.next_word(), !state.cc.z, state), // CNZ
|
||||||
|
0xcc => branch::call_cond(state.next_word(), state.cc.z, state), // CZ
|
||||||
|
0xcd => branch::call(state.next_word(), state), // CALL
|
||||||
|
0xd4 => branch::call_cond(state.next_word(), !state.cc.c, state), // CNC
|
||||||
|
0xdc => branch::call_cond(state.next_word(), state.cc.c, state), // CC
|
||||||
|
0xe4 => branch::call_cond(state.next_word(), !state.cc.p, state), // CPO
|
||||||
|
0xec => branch::call_cond(state.next_word(), state.cc.p, state), // CPE
|
||||||
|
0xf4 => branch::call_cond(state.next_word(), !state.cc.s, state), // CP
|
||||||
|
0xfc => branch::call_cond(state.next_word(), state.cc.s, state), // CM
|
||||||
|
|
||||||
|
// Returns
|
||||||
|
0xc0 => branch::ret_cond(!state.cc.z, state), // RNZ
|
||||||
|
0xc8 => branch::ret_cond(state.cc.z, state), // RZ
|
||||||
|
0xc9 => branch::ret(state), // RET
|
||||||
|
0xd0 => branch::ret_cond(!state.cc.c, state), // RNC
|
||||||
|
0xd8 => branch::ret_cond(state.cc.c, state), // RC
|
||||||
|
0xe0 => branch::ret_cond(!state.cc.p, state), // RPO
|
||||||
|
0xe8 => branch::ret_cond(state.cc.p, state), // RPE
|
||||||
|
0xf0 => branch::ret_cond(!state.cc.s, state), // RP
|
||||||
|
0xf8 => branch::ret_cond(state.cc.s, state), // RM
|
||||||
|
|
||||||
|
// Restarts
|
||||||
|
0xc7 => branch::restart(0, state),
|
||||||
|
0xcf => branch::restart(1, state),
|
||||||
|
0xd7 => branch::restart(2, state),
|
||||||
|
0xdf => branch::restart(3, state),
|
||||||
|
0xe7 => branch::restart(4, state),
|
||||||
|
0xef => branch::restart(5, state),
|
||||||
|
0xf7 => branch::restart(6, state),
|
||||||
|
0xff => branch::restart(7, state),
|
||||||
|
|
||||||
|
// PCHL
|
||||||
|
0xe9 => branch::pchl(state),
|
||||||
|
|
||||||
_ => not_implemented(state),
|
_ => not_implemented(state),
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pc += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_implemented(state: &EmulatorState) {
|
fn not_implemented<M: MemoryMapper>(state: &mut EmulatorState<M>) {
|
||||||
let instruction = state.memory[state.pc as usize];
|
let instruction = state.read_byte(state.pc);
|
||||||
panic!("Unimplemented instruction {:#02X} at {:#04X}", instruction, state.pc);
|
panic!(
|
||||||
|
"Unimplemented instruction {:#02X} at {:#04X}",
|
||||||
|
instruction, state.pc
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
48
src/emulator/mapper/mod.rs
Normal file
48
src/emulator/mapper/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
pub trait MemoryMapper {
|
||||||
|
/// Read a byte at the specified address through the memory mapper
|
||||||
|
fn read(&mut self, address: u16) -> u8;
|
||||||
|
|
||||||
|
/// Write a byte to the specified address through the memory mapper
|
||||||
|
fn write(&mut self, address: u16, value: u8);
|
||||||
|
|
||||||
|
/// Receive a byte from a device on the I/O bus through the memory mapper
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn read_io(&mut self, port: u8) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a byte to a device on the I/O bus through the memory mapper
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn write_io(&mut self, port: u8, value: u8) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[cfg(test)]
|
||||||
|
pub struct TestMapper(pub [u8; u16::MAX as usize + 1]);
|
||||||
|
|
||||||
|
//#[cfg(test)]
|
||||||
|
impl MemoryMapper for TestMapper {
|
||||||
|
fn read(&mut self, address: u16) -> u8 {
|
||||||
|
self.0[address as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, address: u16, value: u8) {
|
||||||
|
self.0[address as usize] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_io(&mut self, port: u8, value: u8) {
|
||||||
|
if port == 0x1 {
|
||||||
|
if value == b'\n' {
|
||||||
|
println!();
|
||||||
|
} else {
|
||||||
|
print!("{}", value as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[cfg(test)]
|
||||||
|
impl Default for TestMapper {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self([0; u16::MAX as usize + 1])
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::MEMORY_SIZE;
|
use crate::mapper::MemoryMapper;
|
||||||
|
|
||||||
pub struct EmulatorState {
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct EmulatorState<M: MemoryMapper> {
|
||||||
pub a: u8,
|
pub a: u8,
|
||||||
pub b: u8,
|
pub b: u8,
|
||||||
pub c: u8,
|
pub c: u8,
|
||||||
|
@ -17,10 +18,121 @@ pub struct EmulatorState {
|
||||||
pub pc: u16,
|
pub pc: u16,
|
||||||
/// Enable interrupts
|
/// Enable interrupts
|
||||||
pub ei: bool,
|
pub ei: bool,
|
||||||
/// Memory map
|
|
||||||
pub memory: [u8; MEMORY_SIZE],
|
/// Memory mapper
|
||||||
|
pub mapper: M,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<M: MemoryMapper> EmulatorState<M> {
|
||||||
|
pub fn new(mapper: M) -> Self {
|
||||||
|
Self {
|
||||||
|
a: 0,
|
||||||
|
b: 0,
|
||||||
|
c: 0,
|
||||||
|
d: 0,
|
||||||
|
e: 0,
|
||||||
|
h: 0,
|
||||||
|
l: 0,
|
||||||
|
sp: 0,
|
||||||
|
cc: ConditionCodes {
|
||||||
|
z: true,
|
||||||
|
s: true,
|
||||||
|
p: true,
|
||||||
|
c: true,
|
||||||
|
},
|
||||||
|
pc: 0,
|
||||||
|
ei: false,
|
||||||
|
mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a byte at a specific address
|
||||||
|
///
|
||||||
|
/// May return an indeterminate value if an invalid region is accessed.
|
||||||
|
/// May cause internal mapper state (eg. interrupts, emulated shift
|
||||||
|
/// registers) to be updated. Subsequent reads from the same address may
|
||||||
|
/// yield different values.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_byte(&mut self, address: u16) -> u8 {
|
||||||
|
self.mapper.read(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a byte at a specific address
|
||||||
|
///
|
||||||
|
/// May do nothing if an invalid region is accessed. Subsequent reads
|
||||||
|
/// from the same address may yield different values.
|
||||||
|
#[inline]
|
||||||
|
pub fn write_byte(&mut self, address: u16, value: u8) {
|
||||||
|
self.mapper.write(address, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next byte from memory pointed at by PC
|
||||||
|
#[inline]
|
||||||
|
pub fn next_byte(&mut self) -> u8 {
|
||||||
|
let value = self.read_byte(self.pc);
|
||||||
|
(self.pc, ..) = self.pc.overflowing_add(1);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a 16-bit word at a specific address
|
||||||
|
///
|
||||||
|
/// May return an indeterminate value if an invalid region is accessed.
|
||||||
|
/// May cause internal mapper state (eg. interrupts, emulated shift
|
||||||
|
/// registers) to be updated. Subsequent reads from the same address may
|
||||||
|
/// yield different values.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_word(&mut self, address: u16) -> u16 {
|
||||||
|
u16::from_le_bytes([
|
||||||
|
self.mapper.read(address),
|
||||||
|
self.mapper.read(address.overflowing_add(1).0),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a 16-bit word at a specific address
|
||||||
|
///
|
||||||
|
/// May do nothing if an invalid region is accessed. Subsequent reads
|
||||||
|
/// from the same address may yield different values.
|
||||||
|
#[inline]
|
||||||
|
pub fn write_word(&mut self, address: u16, value: u16) {
|
||||||
|
let [low, high] = u16::to_le_bytes(value);
|
||||||
|
self.mapper.write(address, low);
|
||||||
|
self.mapper.write(address.overflowing_add(1).0, high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next 16-bit word from memory pointed at by PC, in little endian order
|
||||||
|
#[inline]
|
||||||
|
pub fn next_word(&mut self) -> u16 {
|
||||||
|
let value = self.read_word(self.pc);
|
||||||
|
(self.pc, ..) = self.pc.overflowing_add(2);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive a byte from a device on the I/O bus
|
||||||
|
///
|
||||||
|
/// May return an indeterminate value if an invalid device is accessed.
|
||||||
|
/// May (and usually will) cause internal mapper state (eg. interrupts)
|
||||||
|
/// to be updated.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_io(&mut self, port: u8) -> u8 {
|
||||||
|
self.mapper.read_io(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a byte to a device on the I/O bus
|
||||||
|
///
|
||||||
|
/// May do nothing if an invalid device is accessed.
|
||||||
|
#[inline]
|
||||||
|
pub fn write_io(&mut self, port: u8, value: u8) {
|
||||||
|
self.mapper.write_io(port, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: MemoryMapper + Default> Default for EmulatorState<M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(M::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub struct ConditionCodes {
|
pub struct ConditionCodes {
|
||||||
/// Zero (Z), set if the result is zero.
|
/// Zero (Z), set if the result is zero.
|
||||||
pub z: bool,
|
pub z: bool,
|
||||||
|
@ -35,15 +147,21 @@ pub struct ConditionCodes {
|
||||||
// ac: bool,
|
// ac: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Register {
|
pub enum Register {
|
||||||
B, C, D, E,
|
B,
|
||||||
H, L, M, A,
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
H,
|
||||||
|
L,
|
||||||
|
M,
|
||||||
|
A,
|
||||||
SP,
|
SP,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Register enum based on the input number 0..7 in the order B,C,D,E,H,L,M,A
|
/// Returns a Register enum based on the input number 0..7 in the order B,C,D,E,H,L,M,A
|
||||||
|
#[track_caller]
|
||||||
pub fn register_from_num(b: u8) -> Register {
|
pub fn register_from_num(b: u8) -> Register {
|
||||||
match b {
|
match b {
|
||||||
0 => Register::B,
|
0 => Register::B,
|
||||||
|
@ -58,21 +176,21 @@ pub fn register_from_num(b: u8) -> Register {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_register(register: &Register, state: &EmulatorState) -> u8 {
|
pub fn get_register<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) -> u8 {
|
||||||
match register {
|
match register {
|
||||||
Register::B => state.b as u8,
|
Register::B => state.b,
|
||||||
Register::C => state.c as u8,
|
Register::C => state.c,
|
||||||
Register::D => state.d as u8,
|
Register::D => state.d,
|
||||||
Register::E => state.e as u8,
|
Register::E => state.e,
|
||||||
Register::H => state.h as u8,
|
Register::H => state.h,
|
||||||
Register::L => state.l as u8,
|
Register::L => state.l,
|
||||||
Register::A => state.a as u8,
|
Register::A => state.a,
|
||||||
Register::M => state.memory[u16::from_le_bytes([state.l, state.h]) as usize],
|
Register::M => state.mapper.read(u16::from_le_bytes([state.l, state.h])),
|
||||||
Register::SP => unreachable!(),
|
Register::SP => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_register(register: &Register, value: u8, state: &mut EmulatorState) {
|
pub fn set_register<M: MemoryMapper>(register: Register, value: u8, state: &mut EmulatorState<M>) {
|
||||||
match register {
|
match register {
|
||||||
Register::B => state.b = value,
|
Register::B => state.b = value,
|
||||||
Register::C => state.c = value,
|
Register::C => state.c = value,
|
||||||
|
@ -81,23 +199,119 @@ pub fn set_register(register: &Register, value: u8, state: &mut EmulatorState) {
|
||||||
Register::H => state.h = value,
|
Register::H => state.h = value,
|
||||||
Register::L => state.l = value,
|
Register::L => state.l = value,
|
||||||
Register::A => state.a = value,
|
Register::A => state.a = value,
|
||||||
Register::M => state.memory[u16::from_le_bytes([state.l, state.h]) as usize] = value,
|
Register::M => state
|
||||||
|
.mapper
|
||||||
|
.write(u16::from_le_bytes([state.l, state.h]), value),
|
||||||
Register::SP => panic!("Cannot set 'SP' through set_register()"),
|
Register::SP => panic!("Cannot set 'SP' through set_register()"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_register_pair<M: MemoryMapper>(register: Register, state: &mut EmulatorState<M>) -> u16 {
|
||||||
|
match register {
|
||||||
|
Register::B => u16::from_le_bytes([state.c, state.b]),
|
||||||
|
Register::D => u16::from_le_bytes([state.e, state.d]),
|
||||||
|
Register::H => u16::from_le_bytes([state.l, state.h]),
|
||||||
|
Register::A => {
|
||||||
|
// the PSW looks like this: SZ0A0P1C
|
||||||
|
let flags: u8 = u8::from(state.cc.s) << 7 // bit 7
|
||||||
|
| u8::from(state.cc.z) << 6 // bit 6
|
||||||
|
//| u8::from(state.cc.a) << 4 // bit 4
|
||||||
|
| u8::from(state.cc.p) << 2 // bit 2
|
||||||
|
| 0x02 // bit 1
|
||||||
|
| u8::from(state.cc.c); // bit 0
|
||||||
|
|
||||||
|
u16::from_le_bytes([flags, state.a])
|
||||||
|
}
|
||||||
|
Register::SP => state.sp,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_register_pair<M: MemoryMapper>(
|
||||||
|
register: Register,
|
||||||
|
value: u16,
|
||||||
|
state: &mut EmulatorState<M>,
|
||||||
|
) {
|
||||||
|
let arr = value.to_le_bytes();
|
||||||
|
let high = arr[1];
|
||||||
|
let low = arr[0];
|
||||||
|
match register {
|
||||||
|
Register::B => {
|
||||||
|
state.b = high;
|
||||||
|
state.c = low;
|
||||||
|
}
|
||||||
|
Register::D => {
|
||||||
|
state.d = high;
|
||||||
|
state.e = low;
|
||||||
|
}
|
||||||
|
Register::H => {
|
||||||
|
state.h = high;
|
||||||
|
state.l = low;
|
||||||
|
}
|
||||||
|
Register::A => {
|
||||||
|
state.a = high;
|
||||||
|
// the PSW looks like this: SZ0A0P1C
|
||||||
|
state.cc.s = low & 0b1000_0000 > 0;
|
||||||
|
state.cc.z = low & 0b0100_0000 > 0;
|
||||||
|
debug_assert!(low & 0b0010_0000 == 0, "malformed PSW");
|
||||||
|
//state.cc.a = low & 0b0001_0000 > 0;
|
||||||
|
debug_assert!(low & 0b0000_1000 == 0, "malformed PSW");
|
||||||
|
state.cc.p = low & 0b0000_0100 > 0;
|
||||||
|
debug_assert!(low & 0b0000_0010 > 0, "malformed PSW");
|
||||||
|
state.cc.c = low & 0b0000_0001 > 0;
|
||||||
|
}
|
||||||
|
Register::SP => state.sp = value,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Print values of registers and flags to stdout
|
/// Print values of registers and flags to stdout
|
||||||
pub fn print_state(state: &EmulatorState) {
|
pub fn print_state<M: MemoryMapper>(state: &EmulatorState<M>) {
|
||||||
// State
|
// State
|
||||||
println!("\nsp\tpc\tei");
|
println!("\nsp\tpc\tei");
|
||||||
println!("{:#06x}\t{:#06x}\t{}", state.sp, state.pc, state.ei);
|
println!("{:#06x}\t{:#06x}\t{}", state.sp, state.pc, state.ei);
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
println!("\nB\tC\tD\tE\tH\tL\tA");
|
println!("\nB\tC\tD\tE\tH\tL\tA");
|
||||||
println!("{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}",
|
println!(
|
||||||
state.b, state.c, state.d, state.e, state.h, state.l, state.a);
|
"{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}\t{:#04x}",
|
||||||
|
state.b, state.c, state.d, state.e, state.h, state.l, state.a
|
||||||
|
);
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
println!("\nz\ts\tp\tc");
|
println!("\nz\ts\tp\tc");
|
||||||
println!("{}\t{}\t{}\t{}", state.cc.z, state.cc.s, state.cc.p, state.cc.c);
|
println!(
|
||||||
|
"{}\t{}\t{}\t{}",
|
||||||
|
state.cc.z, state.cc.s, state.cc.p, state.cc.c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::mapper::TestMapper;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_word() {
|
||||||
|
let mut state = EmulatorState::<TestMapper>::default();
|
||||||
|
state.mapper.0[0x10] = 0x34;
|
||||||
|
state.mapper.0[0x11] = 0x12;
|
||||||
|
assert_eq!(state.read_word(0x10), 0x1234);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_word() {
|
||||||
|
let mut state = EmulatorState::<TestMapper>::default();
|
||||||
|
state.write_word(0x10, 0x1234);
|
||||||
|
assert_eq!(state.mapper.0[0x10..=0x11], [0x34, 0x12]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn next_word_at_pc() {
|
||||||
|
let mut state = EmulatorState::<TestMapper>::default();
|
||||||
|
state.mapper.0[0x10] = 0x34;
|
||||||
|
state.mapper.0[0x11] = 0x12;
|
||||||
|
state.pc = 0x10;
|
||||||
|
assert_eq!(state.next_word(), 0x1234);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue