Interrupt system implemented, some new programs.
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
Tests/build/
|
Tests/build/
|
||||||
|
Programs/build/
|
||||||
Object/
|
Object/
|
||||||
Assembler
|
Assembler
|
||||||
SplitBit
|
SplitBit
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
resume
|
||||||
|
codexResume.sh
|
||||||
|
|||||||
@@ -132,21 +132,7 @@ DecimalValue:
|
|||||||
0x00 ; The tens place.
|
0x00 ; The tens place.
|
||||||
0x00 ; The hundreds place.
|
0x00 ; The hundreds place.
|
||||||
|
|
||||||
; For convenience, I'll pad this out so programs using this library store their data in a fresh page of memory.
|
; This library used to pad its data out to a whole page, so that anything including it
|
||||||
|
; started on a page boundary. That cost every program 253 bytes of zeroes to give one
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
; program a guarantee it could not ask for out loud. A program that needs a boundary now
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
; writes #Align in front of the thing that needs it.
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
; Conway's Game of Life rewritten for SplitBit's four-Data-Pointer ISA.
|
||||||
|
;
|
||||||
|
; The representation and display match 16x16Life.asm: a visible 16x16 field,
|
||||||
|
; a dead border, and interleaved current/next bytes. DP0 walks the board, DP1
|
||||||
|
; and DP2 address the loop counters, and volatile DP3 walks the neighborhood.
|
||||||
|
|
||||||
|
#Include print.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CALL seedGlider
|
||||||
|
SETD.0 ClearScreen
|
||||||
|
CALL printString
|
||||||
|
|
||||||
|
generationLoop:
|
||||||
|
CALL renderBoard
|
||||||
|
CALL evolveBoard
|
||||||
|
CALL commitBoard
|
||||||
|
CALL frameDelay
|
||||||
|
BRI generationLoop
|
||||||
|
|
||||||
|
seedGlider:
|
||||||
|
SETD.0 Board
|
||||||
|
DPUP.0 0d42
|
||||||
|
INIA 0x01
|
||||||
|
STA.0
|
||||||
|
SETD.0 Board
|
||||||
|
DPUP.0 0d80
|
||||||
|
STA.0
|
||||||
|
SETD.0 Board
|
||||||
|
DPUP.0 0d112
|
||||||
|
STA.0
|
||||||
|
DPUP.0 0d02
|
||||||
|
STA.0
|
||||||
|
DPUP.0 0d02
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
renderBoard:
|
||||||
|
SETD.0 CursorHome
|
||||||
|
CALL printString
|
||||||
|
SETD.1 RowCount
|
||||||
|
SETD.2 ColCount
|
||||||
|
INIA 0d16
|
||||||
|
STA.1
|
||||||
|
SETD.0 Board
|
||||||
|
DPUP.0 0d38
|
||||||
|
|
||||||
|
renderRow:
|
||||||
|
INIA 0d16
|
||||||
|
STA.2
|
||||||
|
renderCell:
|
||||||
|
LDA.0
|
||||||
|
BRA renderDead
|
||||||
|
INIB 0x23
|
||||||
|
OUTB 0x00
|
||||||
|
BRI renderCellDone
|
||||||
|
renderDead:
|
||||||
|
INIB 0x20
|
||||||
|
OUTB 0x00
|
||||||
|
renderCellDone:
|
||||||
|
DPUP.0 0d02
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
BRA renderRowDone
|
||||||
|
BRI renderCell
|
||||||
|
|
||||||
|
renderRowDone:
|
||||||
|
CALL lineFeed
|
||||||
|
DPUP.0 0d04
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BRA renderDone
|
||||||
|
BRI renderRow
|
||||||
|
renderDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
evolveBoard:
|
||||||
|
SETD.1 RowCount
|
||||||
|
SETD.2 ColCount
|
||||||
|
INIA 0d16
|
||||||
|
STA.1
|
||||||
|
SETD.0 Board
|
||||||
|
DPUP.0 0d38
|
||||||
|
|
||||||
|
evolveRow:
|
||||||
|
INIA 0d16
|
||||||
|
STA.2
|
||||||
|
evolveCellLoop:
|
||||||
|
CALL evolveCell
|
||||||
|
DPUP.0 0d02
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
BRA evolveRowDone
|
||||||
|
BRI evolveCellLoop
|
||||||
|
|
||||||
|
evolveRowDone:
|
||||||
|
DPUP.0 0d04
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BRA evolveDone
|
||||||
|
BRI evolveRow
|
||||||
|
evolveDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
evolveCell:
|
||||||
|
CALL countNeighbors
|
||||||
|
MVQB ; B is the neighbor count from here down.
|
||||||
|
|
||||||
|
; Three neighbors always produces a live cell.
|
||||||
|
INIA 0d03
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ makeAlive
|
||||||
|
|
||||||
|
; Two neighbors preserve the current state.
|
||||||
|
INIA 0d02
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ preserveCell
|
||||||
|
|
||||||
|
makeDead:
|
||||||
|
RSTA
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
DECD.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
preserveCell:
|
||||||
|
LDA.0
|
||||||
|
BRA makeDead
|
||||||
|
|
||||||
|
makeAlive:
|
||||||
|
INIA 0x01
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
DECD.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Return the eight-neighbor sum in Q. One Stack round-trip copies DP0 into
|
||||||
|
; volatile DP3; MVQA then keeps the running total entirely in registers.
|
||||||
|
countNeighbors:
|
||||||
|
PSHD.0
|
||||||
|
POPD.3
|
||||||
|
RSTA
|
||||||
|
DPDN.3 0d38
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d02
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d02
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d32
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d04
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d32
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d02
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
DPUP.3 0d02
|
||||||
|
|
||||||
|
LDB.3
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
commitBoard:
|
||||||
|
SETD.1 RowCount
|
||||||
|
SETD.2 ColCount
|
||||||
|
INIA 0d18
|
||||||
|
STA.1
|
||||||
|
SETD.0 Board
|
||||||
|
|
||||||
|
commitRow:
|
||||||
|
INIA 0d18
|
||||||
|
STA.2
|
||||||
|
commitCell:
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
DECD.0
|
||||||
|
STA.0
|
||||||
|
DPUP.0 0d02
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
BRA commitRowDone
|
||||||
|
BRI commitCell
|
||||||
|
|
||||||
|
commitRowDone:
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BRA commitDone
|
||||||
|
BRI commitRow
|
||||||
|
commitDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
frameDelay:
|
||||||
|
INIA 0xFF
|
||||||
|
delayOuter:
|
||||||
|
INIB 0xFF
|
||||||
|
delayInner:
|
||||||
|
DECB
|
||||||
|
BRB delayInnerDone
|
||||||
|
BRI delayInner
|
||||||
|
delayInnerDone:
|
||||||
|
DECA
|
||||||
|
BRA delayDone
|
||||||
|
BRI delayOuter
|
||||||
|
delayDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
RowCount:
|
||||||
|
0x00
|
||||||
|
ColCount:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
ClearScreen:
|
||||||
|
0x1B
|
||||||
|
"[2J"
|
||||||
|
CursorHome:
|
||||||
|
0x1B
|
||||||
|
"[H"
|
||||||
|
|
||||||
|
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
|
||||||
|
; original leaves this implicit and leans on Data Memory being zero, which works but
|
||||||
|
; means the assembler believes the board is one byte long: anything placed after it
|
||||||
|
; would land inside it, and nothing would say so. Reserving the region states how far
|
||||||
|
; it reaches, so a label added below here is safe.
|
||||||
|
Board:
|
||||||
|
#Reserve 0d648
|
||||||
+3
-1
@@ -28,7 +28,9 @@ PROGRAMS = \
|
|||||||
Fibonacci/32bitFibonacci.asm \
|
Fibonacci/32bitFibonacci.asm \
|
||||||
primeSieve/8bitSieve.asm \
|
primeSieve/8bitSieve.asm \
|
||||||
primeSieve/16bitSegmentedSieve.asm \
|
primeSieve/16bitSegmentedSieve.asm \
|
||||||
gameOfLife/16x16Life.asm
|
primeSieve/16bitSegmentedSieveModern.asm \
|
||||||
|
gameOfLife/16x16Life.asm \
|
||||||
|
gameOfLife/16x16LifeModern.asm
|
||||||
|
|
||||||
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
|
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
|
||||||
DEPENDENCIES = $(BINARIES:.bin=.d)
|
DEPENDENCIES = $(BINARIES:.bin=.d)
|
||||||
|
|||||||
@@ -157,7 +157,12 @@ printCandidateHex:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
; print.asm deliberately pads its data to one page, so this begins at 0x0100.
|
; This has to begin on a page boundary, and now says so itself rather than relying on
|
||||||
|
; whatever happens to have been assembled before it. The marking loop treats a carry out
|
||||||
|
; of the low byte as the end of the page, which only finds the right boundary if the
|
||||||
|
; window starts on one.
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
Segment:
|
Segment:
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
|
||||||
|
;
|
||||||
|
; This deliberately implements the same algorithm and emits the same text as
|
||||||
|
; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison.
|
||||||
|
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
|
||||||
|
; marks multiples. CALL preserves the first three pointers automatically.
|
||||||
|
|
||||||
|
#Include print.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
RSTA
|
||||||
|
SETD.1 Page
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
nextPage:
|
||||||
|
SETD.2 Segment
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
clearSegment:
|
||||||
|
STB.2
|
||||||
|
INCD.2
|
||||||
|
INCA
|
||||||
|
BRA segmentCleared
|
||||||
|
BRI clearSegment
|
||||||
|
|
||||||
|
segmentCleared:
|
||||||
|
; Zero and one are not prime.
|
||||||
|
LDA.1
|
||||||
|
BRA excludeZeroAndOne
|
||||||
|
BRI markSegment
|
||||||
|
excludeZeroAndOne:
|
||||||
|
SETD.2 Segment
|
||||||
|
INIA 0x01
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
|
||||||
|
markSegment:
|
||||||
|
SETD.0 PrimeStates
|
||||||
|
INIA 0d54
|
||||||
|
primeLoop:
|
||||||
|
CALL processPrime
|
||||||
|
DPUP.0 0d03
|
||||||
|
DECA
|
||||||
|
BRA scanSegment
|
||||||
|
BRI primeLoop
|
||||||
|
|
||||||
|
scanSegment:
|
||||||
|
SETD.2 Segment
|
||||||
|
RSTA
|
||||||
|
scanLoop:
|
||||||
|
LDB.2
|
||||||
|
BRB emitPrime
|
||||||
|
scanNext:
|
||||||
|
INCD.2
|
||||||
|
INCA
|
||||||
|
BRA advancePage
|
||||||
|
BRI scanLoop
|
||||||
|
|
||||||
|
emitPrime:
|
||||||
|
CALL printCandidateHex
|
||||||
|
BRI scanNext
|
||||||
|
|
||||||
|
advancePage:
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
BRA finished
|
||||||
|
BRI nextPage
|
||||||
|
|
||||||
|
finished:
|
||||||
|
CALL lineFeed
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; DP0 points at a PrimeStates entry. CALL restores it on return.
|
||||||
|
processPrime:
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
LDB.1
|
||||||
|
XOR
|
||||||
|
BRQ primeIsActive
|
||||||
|
RET
|
||||||
|
|
||||||
|
primeIsActive:
|
||||||
|
; B is the prime and A its current offset.
|
||||||
|
DECD.0
|
||||||
|
LDB.0
|
||||||
|
DPUP.0 0d02
|
||||||
|
LDA.0
|
||||||
|
|
||||||
|
; DP3 = Segment + offset. Only this one initial pointer copy needs the Stack.
|
||||||
|
SETD.3 Segment
|
||||||
|
PSHB
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
PSHQ
|
||||||
|
POPD.3
|
||||||
|
POPB
|
||||||
|
|
||||||
|
markPrimeLoop:
|
||||||
|
INIA 0x01
|
||||||
|
STA.3
|
||||||
|
|
||||||
|
; Add the prime to DP3's low byte. A carry crosses into the next window.
|
||||||
|
PSHD.3
|
||||||
|
POPA
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
PSHQ
|
||||||
|
POPD.3
|
||||||
|
BRC primeFinished
|
||||||
|
BRI markPrimeLoop
|
||||||
|
|
||||||
|
primeFinished:
|
||||||
|
; DP0 is on the offset byte; advance the saved high byte and save Q as
|
||||||
|
; the wrapped offset for the following page.
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STQ.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
printCandidateHex:
|
||||||
|
PSHA
|
||||||
|
LDA.1
|
||||||
|
CALL printByteHex
|
||||||
|
POPA
|
||||||
|
CALL printByteHex
|
||||||
|
CALL blankSpace
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; Segment has to begin on a page boundary, and now says so itself rather than relying on
|
||||||
|
; whatever happens to have been assembled before it. The marking loop adds the prime to
|
||||||
|
; the low byte of DP3 and treats the carry out as the end of the page, so it only finds
|
||||||
|
; the right boundary if the window starts on one.
|
||||||
|
;
|
||||||
|
; The whole window is written out here so that Page and PrimeStates begin after it.
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
|
Segment:
|
||||||
|
0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
|
||||||
|
Page:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
PrimeStates:
|
||||||
|
0x02 0x00 0x04
|
||||||
|
0x03 0x00 0x09
|
||||||
|
0x05 0x00 0x19
|
||||||
|
0x07 0x00 0x31
|
||||||
|
0x0B 0x00 0x79
|
||||||
|
0x0D 0x00 0xA9
|
||||||
|
0x11 0x01 0x21
|
||||||
|
0x13 0x01 0x69
|
||||||
|
0x17 0x02 0x11
|
||||||
|
0x1D 0x03 0x49
|
||||||
|
0x1F 0x03 0xC1
|
||||||
|
0x25 0x05 0x59
|
||||||
|
0x29 0x06 0x91
|
||||||
|
0x2B 0x07 0x39
|
||||||
|
0x2F 0x08 0xA1
|
||||||
|
0x35 0x0A 0xF9
|
||||||
|
0x3B 0x0D 0x99
|
||||||
|
0x3D 0x0E 0x89
|
||||||
|
0x43 0x11 0x89
|
||||||
|
0x47 0x13 0xB1
|
||||||
|
0x49 0x14 0xD1
|
||||||
|
0x4F 0x18 0x61
|
||||||
|
0x53 0x1A 0xE9
|
||||||
|
0x59 0x1E 0xF1
|
||||||
|
0x61 0x24 0xC1
|
||||||
|
0x65 0x27 0xD9
|
||||||
|
0x67 0x29 0x71
|
||||||
|
0x6B 0x2C 0xB9
|
||||||
|
0x6D 0x2E 0x69
|
||||||
|
0x71 0x31 0xE1
|
||||||
|
0x7F 0x3F 0x01
|
||||||
|
0x83 0x43 0x09
|
||||||
|
0x89 0x49 0x51
|
||||||
|
0x8B 0x4B 0x79
|
||||||
|
0x95 0x56 0xB9
|
||||||
|
0x97 0x59 0x11
|
||||||
|
0x9D 0x60 0x49
|
||||||
|
0xA3 0x67 0xC9
|
||||||
|
0xA7 0x6C 0xF1
|
||||||
|
0xAD 0x74 0xE9
|
||||||
|
0xB3 0x7D 0x29
|
||||||
|
0xB5 0x7F 0xF9
|
||||||
|
0xBF 0x8E 0x81
|
||||||
|
0xC1 0x91 0x81
|
||||||
|
0xC5 0x97 0x99
|
||||||
|
0xC7 0x9A 0xB1
|
||||||
|
0xD3 0xAD 0xE9
|
||||||
|
0xDF 0xC2 0x41
|
||||||
|
0xE3 0xC9 0x49
|
||||||
|
0xE5 0xCC 0xD9
|
||||||
|
0xE9 0xD4 0x11
|
||||||
|
0xEF 0xDF 0x21
|
||||||
|
0xF1 0xE2 0xE1
|
||||||
|
0xFB 0xF6 0x19
|
||||||
@@ -43,7 +43,11 @@ start:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
; The table of our prime candidates.
|
; The table of our prime candidates. It has to begin on a page boundary: marking walks
|
||||||
|
; the pointer's low byte and treats the carry out as running off the end of the table,
|
||||||
|
; which only finds the right end if the table starts on one.
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
DataTop:
|
DataTop:
|
||||||
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
; Tests a hardware interrupt delivered to a handler named in the Vector Segment.
|
||||||
|
;
|
||||||
|
; A device is named by the port it is plugged into, because that is what decides which
|
||||||
|
; vector it arrives through. The test device on port 0x10 puts its line up when anything
|
||||||
|
; is written to it.
|
||||||
|
;
|
||||||
|
; The device is asked for attention while the Interrupt Flag is down, so the line waits.
|
||||||
|
; SIF lets it through, and the handler runs before the next instruction does.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; O printed with the line up and the flag down
|
||||||
|
; K printed by the handler
|
||||||
|
; ! printed after RETI came back
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CIF ; Hold devices off.
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x10 ; The device asks. Its line goes up and stays up.
|
||||||
|
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
SIF ; Let it through. It is answered on the very next step.
|
||||||
|
|
||||||
|
INIA 0d33 ; '!'. Reached only because RETI came back here.
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
deviceHandler:
|
||||||
|
INIA 0d75 ; 'K'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Device 0x10 deviceHandler
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
; #Align before either segment has been opened.
|
||||||
|
;
|
||||||
|
; The directive moves a cursor along, and outside a segment there is no cursor for
|
||||||
|
; it to move, so this has to be an error rather than quietly doing nothing.
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
; #Align with no number after it.
|
||||||
|
;
|
||||||
|
; Without this check the next token is taken as the alignment, which would silently
|
||||||
|
; align to whatever the following instruction happened to be worth.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
HALT
|
||||||
|
#Align
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
; Deliberately broken, to check that the assembler still catches it.
|
||||||
|
; The file ends straight after the keyword, so there is no name to read. This
|
||||||
|
; used to walk into whatever the array happened to hold and crash.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Include
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
; SWI with nothing after it.
|
||||||
|
;
|
||||||
|
; Without the operand check this assembles, and SWI quietly takes the next
|
||||||
|
; instruction as its vector number. Everything after it then shifts by a byte.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SWI
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
; Deliberately broken, to check that the assembler still catches it.
|
||||||
|
; The same name is defined twice, which used to be accepted silently, with every
|
||||||
|
; reference quietly resolving to whichever definition came first.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
BRI twice
|
||||||
|
|
||||||
|
twice:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
twice:
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
; Two handlers claiming the same vector.
|
||||||
|
;
|
||||||
|
; Device lines name a port directly, so two of them can collide even though the
|
||||||
|
; assembler numbers the named vectors itself.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
firstHandler:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
secondHandler:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Device 0x10 firstHandler
|
||||||
|
Device 0x10 secondHandler
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
; SWI names something that was never given a handler.
|
||||||
|
;
|
||||||
|
; A vector name is not a label, so the usual "undefined label" error would be
|
||||||
|
; misleading. It has to say that the name needs a #Vectors entry.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SWI neverDeclared
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
; Tests BRD, the only branch whose destination is not written into the program.
|
||||||
|
;
|
||||||
|
; A table of addresses in the Data Segment is walked with one Data Pointer, each
|
||||||
|
; entry is pulled out with LDD, and BRD jumps to it. That is dispatch: choosing
|
||||||
|
; where to go from data rather than from a branch the assembler laid down.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; one
|
||||||
|
; two
|
||||||
|
; three
|
||||||
|
; done
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Handlers ; DP0 walks the table of handler addresses.
|
||||||
|
INIB 0d3 ; Three of them.
|
||||||
|
|
||||||
|
dispatchLoop:
|
||||||
|
LDD.1.0 ; DP1 becomes the address of the next handler.
|
||||||
|
BRD.1 ; Go there. The handler branches back to itself.
|
||||||
|
|
||||||
|
; Each handler prints its name and returns to the loop by hand. There is no CALL
|
||||||
|
; here on purpose, so that what BRD does is the only thing under test.
|
||||||
|
handlerOne:
|
||||||
|
SETD.2 One
|
||||||
|
CALL printDP2
|
||||||
|
BRI nextHandler
|
||||||
|
|
||||||
|
handlerTwo:
|
||||||
|
SETD.2 Two
|
||||||
|
CALL printDP2
|
||||||
|
BRI nextHandler
|
||||||
|
|
||||||
|
handlerThree:
|
||||||
|
SETD.2 Three
|
||||||
|
CALL printDP2
|
||||||
|
BRI nextHandler
|
||||||
|
|
||||||
|
nextHandler:
|
||||||
|
DPUP.0 0d02 ; Step over the two byte table entry.
|
||||||
|
DECB
|
||||||
|
BRB finished
|
||||||
|
BRI dispatchLoop
|
||||||
|
|
||||||
|
finished:
|
||||||
|
SETD.2 Done
|
||||||
|
CALL printDP2
|
||||||
|
HALT
|
||||||
|
|
||||||
|
printDP2:
|
||||||
|
LDA.2
|
||||||
|
BRA printDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.2
|
||||||
|
BRI printDP2
|
||||||
|
printDone:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
One:
|
||||||
|
"one"
|
||||||
|
Two:
|
||||||
|
"two"
|
||||||
|
Three:
|
||||||
|
"three"
|
||||||
|
Done:
|
||||||
|
"done"
|
||||||
|
|
||||||
|
; The dispatch table. Each name becomes the address of that handler.
|
||||||
|
Handlers:
|
||||||
|
handlerOne
|
||||||
|
handlerTwo
|
||||||
|
handlerThree
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
; Tests a fault handler that steps over the byte it could not decode and carries on.
|
||||||
|
;
|
||||||
|
; The frame holds the address of the offending byte rather than the one after it, so a
|
||||||
|
; handler can see exactly what failed. The cost is that returning with a bare RETI meets
|
||||||
|
; the same byte again, which is why this handler moves the saved address on by one
|
||||||
|
; first. MVSD is what lets it reach the frame at all.
|
||||||
|
;
|
||||||
|
; 0xFE is not an instruction. Writing it as a literal is the only way past the
|
||||||
|
; assembler, which is what makes a program containing one buildable.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; O printed before the byte that fails
|
||||||
|
; K printed after the handler stepped over it
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
0xFE ; Not an instruction. The handler steps over this.
|
||||||
|
|
||||||
|
INIA 0d75 ; 'K'. Reached only because the handler moved the address on.
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
faultHandler:
|
||||||
|
; The frame sits above the Stack Pointer, which points at the next free slot. The
|
||||||
|
; address to resume at went on first, so it is furthest up: high byte 13 above the
|
||||||
|
; Stack Pointer, low byte 14 above.
|
||||||
|
MVSD.0
|
||||||
|
DPUP.0 0d14
|
||||||
|
LDA.0
|
||||||
|
INCA ; Step past the one byte that failed.
|
||||||
|
STA.0
|
||||||
|
BRC carried ; The low byte wrapped, so the high byte needs the carry.
|
||||||
|
RETI
|
||||||
|
|
||||||
|
carried:
|
||||||
|
DPDN.0 0d01
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
BadOpcode faultHandler
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
; Tests that the CPU stops when it meets a byte it cannot decode.
|
||||||
|
;
|
||||||
|
; 0xFE is not an instruction. Placing it in the Program Segment as a literal gets
|
||||||
|
; it past the assembler, which is the only way to build a program containing one.
|
||||||
|
;
|
||||||
|
; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing
|
||||||
|
; at the offending byte rather than stepping over it and carrying on. The emulator
|
||||||
|
; then reports what it was and where, and exits non zero.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
INIA 0d65 ; Something harmless first, so the fault is not at address zero.
|
||||||
|
0xFE ; Not an instruction.
|
||||||
|
HALT ; Never reached.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
; The worked example from the SplitBit Assembler Manual, kept here so that the manual
|
||||||
|
; cannot quietly stop being true. If this test changes, the manual changes with it.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; ready
|
||||||
|
; trap
|
||||||
|
; device
|
||||||
|
|
||||||
|
; Interrupt handling from all three directions.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CIF ; Hold devices off while we set up.
|
||||||
|
SETD.0 Greeting
|
||||||
|
CALL printString
|
||||||
|
|
||||||
|
SWI announce ; A trap of our own, reached by name.
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x10 ; Ask the test device for attention. Its line goes up.
|
||||||
|
SIF ; Let it through. It is answered before the next instruction.
|
||||||
|
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; A trap. It is entered with a full frame, so it may use any register it likes
|
||||||
|
; without agreeing anything with the code it interrupted.
|
||||||
|
announce:
|
||||||
|
SETD.0 Trapped
|
||||||
|
CALL printString
|
||||||
|
RETI
|
||||||
|
|
||||||
|
; The device handler. Reached because the device sits on port 0x10.
|
||||||
|
deviceReady:
|
||||||
|
SETD.0 Device
|
||||||
|
CALL printString
|
||||||
|
RETI
|
||||||
|
|
||||||
|
; The fault handler. It reports and stops, rather than trying to carry on.
|
||||||
|
reportFault:
|
||||||
|
SETD.0 Broken
|
||||||
|
CALL printString
|
||||||
|
HALT
|
||||||
|
|
||||||
|
printString: ; Expects DP0 to be set to the beginning of the string.
|
||||||
|
LDA.0
|
||||||
|
BRA printDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI printString
|
||||||
|
printDone:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Greeting:
|
||||||
|
"ready"
|
||||||
|
Trapped:
|
||||||
|
"trap"
|
||||||
|
Device:
|
||||||
|
"device"
|
||||||
|
Broken:
|
||||||
|
"bad opcode"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start ; Begin here rather than at the first byte.
|
||||||
|
BadOpcode reportFault
|
||||||
|
announce announce ; A name of our own. The assembler numbers it.
|
||||||
|
Device 0x10 deviceReady ; Named by the port, because that is what decides it.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
; Tests SIF and CIF, the Interrupt Flag.
|
||||||
|
;
|
||||||
|
; Nothing reads the Interrupt Flag yet, so what this proves is that the two
|
||||||
|
; instructions decode, execute, and leave everything else exactly as they found
|
||||||
|
; it. The Carry Flag is the part worth checking, because it lives in the same
|
||||||
|
; Status register and a careless mask would take it out.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; OKC
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Load two registers, run the new instructions across them, and prove that
|
||||||
|
; nothing moved.
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
INIB 0d75 ; 'K'
|
||||||
|
SIF
|
||||||
|
CIF
|
||||||
|
SIF
|
||||||
|
OUTA 0x00 ; Still 'O'.
|
||||||
|
OUTB 0x00 ; Still 'K'.
|
||||||
|
|
||||||
|
; Now set the Carry Flag, and work the Interrupt Flag around it.
|
||||||
|
CCF
|
||||||
|
INIA 0xFF
|
||||||
|
INIB 0x01
|
||||||
|
ADD ; Q = 0, and the Carry Flag is set.
|
||||||
|
SIF
|
||||||
|
CIF
|
||||||
|
BRC carryHeld
|
||||||
|
|
||||||
|
; Falling through here means one flag trampled the other.
|
||||||
|
INIA 0d88 ; 'X'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
carryHeld:
|
||||||
|
INIA 0d67 ; 'C'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
; Tests that the Interrupt Flag holds a device off, and that the device is still
|
||||||
|
; waiting once the flag goes back up.
|
||||||
|
;
|
||||||
|
; The test device on port 0x10 puts its own line up when anything is written to it. A
|
||||||
|
; device on port N interrupts on N, so this one arrives on hardware vector 16.
|
||||||
|
;
|
||||||
|
; Nothing is installed at that vector, so answering it is a fault. That is the point:
|
||||||
|
; the fault is proof the line was answered, and where it appears in the output is proof
|
||||||
|
; of when. The letters are printed to mark how far the program got.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; M the device has asked, and the flag is down, so nothing has happened
|
||||||
|
; S still nothing, several instructions later
|
||||||
|
; then a fault naming hardware vector 16, raised after SIF and not before.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CIF ; Hold devices off.
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x10 ; The device asks for attention. Its line goes up and stays up.
|
||||||
|
|
||||||
|
INIA 0d77 ; 'M', printed with the line still up and the flag still down.
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
NOP ; Several instructions pass and the line is still not answered.
|
||||||
|
NOP
|
||||||
|
NOP
|
||||||
|
|
||||||
|
INIA 0d83 ; 'S'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
SIF ; Now let it through. The line is answered on the very next step.
|
||||||
|
|
||||||
|
; Never reached.
|
||||||
|
INIA 0d88 ; 'X'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
; Tests MVQA and MVQB.
|
||||||
|
;
|
||||||
|
; Every ALU result lands in Q, and Q is not an ALU operand. Without these two
|
||||||
|
; instructions the only way to use a result in the next sum is to store it into
|
||||||
|
; Data Memory and load it back, which costs two instructions and a Data Pointer
|
||||||
|
; that has to be pointing somewhere sensible.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; AAA
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CCF
|
||||||
|
INIA 0d60
|
||||||
|
INIB 0d5
|
||||||
|
ADD ; Q = 65, which is 'A'.
|
||||||
|
MVQA ; A = 65
|
||||||
|
OUTA 0x00
|
||||||
|
MVQB ; And B, from the same result.
|
||||||
|
OUTB 0x00
|
||||||
|
|
||||||
|
; A running total kept entirely in registers, which is the thing that was not
|
||||||
|
; possible before. Nothing here touches Data Memory at all.
|
||||||
|
CCF
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
ADD ; Q = 1
|
||||||
|
MVQA
|
||||||
|
ADD ; Q = 2
|
||||||
|
MVQA
|
||||||
|
ADD ; Q = 3
|
||||||
|
MVQA
|
||||||
|
INIB 0d62
|
||||||
|
ADD ; Q = 65 again
|
||||||
|
MVQA
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
; Tests #Align and #Reserve by printing the addresses they produce.
|
||||||
|
;
|
||||||
|
; Both directives only move the cursor along, so what they do is entirely visible in
|
||||||
|
; where the labels after them land. The program pushes each pointer and prints the two
|
||||||
|
; bytes of its address, which is the only way a SplitBit program can look at one.
|
||||||
|
;
|
||||||
|
; Aligned is asked for on a page boundary, so its low byte has to be 0x00. Reserved
|
||||||
|
; follows one byte of data and a reservation of 0x30, so it lands 0x31 further on.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; 00 31
|
||||||
|
|
||||||
|
#Include print.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; The aligned label. Only the low byte is interesting: a page boundary means zero.
|
||||||
|
SETD.0 Aligned
|
||||||
|
PSHD.0 ; High byte, then low, so the low byte comes off first.
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
CALL printByteHex
|
||||||
|
CALL blankSpace
|
||||||
|
|
||||||
|
; The reserved region. Reserved sits one byte of data plus 0x30 reserved bytes past
|
||||||
|
; Aligned, so its low byte says how far the reservation moved the cursor.
|
||||||
|
SETD.0 Reserved
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
CALL printByteHex
|
||||||
|
CALL lineFeed
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
|
Aligned:
|
||||||
|
0x41 ; One byte of real data, so the cursor is one past the boundary.
|
||||||
|
#Reserve 0x30
|
||||||
|
Reserved:
|
||||||
|
0x42
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
; Tests MVSD, which copies the Stack Pointer into a Data Pointer.
|
||||||
|
;
|
||||||
|
; The Stack Pointer still cannot be written, so this does not let a program move the
|
||||||
|
; Stack. It lets a program find it, which is what reading anything already on the Stack
|
||||||
|
; requires. Without it, the manual's claim that a Data Pointer can be aimed at the Stack
|
||||||
|
; is not something a program can actually act on: there is no way to learn where the
|
||||||
|
; Stack is without already knowing.
|
||||||
|
;
|
||||||
|
; An interrupt handler needs this to reach its own frame, which is how a fault handler
|
||||||
|
; steps over the byte that failed and carries on.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; OK
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Push two bytes, then go looking for them.
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
PSHA
|
||||||
|
INIA 0d75 ; 'K'
|
||||||
|
PSHA
|
||||||
|
|
||||||
|
; The Stack Pointer points at the next free slot, so the byte pushed last sits one
|
||||||
|
; above it, and the one before that sits two above.
|
||||||
|
MVSD.0
|
||||||
|
DPUP.0 0d01
|
||||||
|
LDA.0 ; 'K', the last one pushed.
|
||||||
|
INCD.0
|
||||||
|
LDB.0 ; 'O', the one before it.
|
||||||
|
|
||||||
|
OUTB 0x00
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
; Tests a software interrupt that names a vector with nothing installed in it.
|
||||||
|
;
|
||||||
|
; Until the assembler can lay down a vector table, every entry reads as zero, and zero
|
||||||
|
; means no handler. Dispatching through one has to stop the machine and say which
|
||||||
|
; vector was empty, rather than jumping to the bottom of Program Memory and running
|
||||||
|
; whatever happens to be sitting there.
|
||||||
|
;
|
||||||
|
; The letter is printed first so that it is obvious the program ran at all, and that it
|
||||||
|
; stopped exactly where it should have.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; O
|
||||||
|
; a fault naming software vector 20, and a non zero exit.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
SWI 0d20 ; Nothing is installed here.
|
||||||
|
|
||||||
|
; Never reached. If the machine ever prints this, the empty vector was taken.
|
||||||
|
INIA 0d88 ; 'X'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
; Tests the Vector Segment: a Boot Vector pointing somewhere other than the start of the
|
||||||
|
; program, and a software interrupt the program names for itself.
|
||||||
|
;
|
||||||
|
; The decoy sits at address 0x0000, where execution would begin if the Boot Vector were
|
||||||
|
; not obeyed. It prints an X, so if an X ever appears the vector was ignored.
|
||||||
|
;
|
||||||
|
; The handler tramples every register it can reach. Everything printed afterwards comes
|
||||||
|
; out of the frame, which is the point: an interrupt gives back what it borrowed, and
|
||||||
|
; unlike a subroutine that includes Q and Data Pointer 3.
|
||||||
|
;
|
||||||
|
; Correct output is:
|
||||||
|
; OK!
|
||||||
|
; good
|
||||||
|
; AFTER
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
decoy:
|
||||||
|
INIA 0d88 ; 'X'. Never reached.
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
realStart:
|
||||||
|
CCF
|
||||||
|
INIA 0d79 ; 'O'
|
||||||
|
INIB 0d75 ; 'K'
|
||||||
|
SETD.0 Good ; DP0 is preserved across a CALL as well.
|
||||||
|
SETD.3 After ; DP3 is not, but an interrupt has to give it back anyway.
|
||||||
|
|
||||||
|
SWI stampTrap
|
||||||
|
|
||||||
|
OUTA 0x00 ; 'O'
|
||||||
|
OUTB 0x00 ; 'K'
|
||||||
|
BRC carryLost
|
||||||
|
INIA 0d33 ; '!', so the Status register came back too.
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
CALL printDP0
|
||||||
|
CALL printDP3
|
||||||
|
HALT
|
||||||
|
|
||||||
|
carryLost:
|
||||||
|
INIA 0d63 ; '?'
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; The handler. Nothing it does to a register should survive.
|
||||||
|
stampRegisters:
|
||||||
|
INIA 0xFF
|
||||||
|
INIB 0x01
|
||||||
|
ADD ; Q is stamped, and the Carry Flag is set.
|
||||||
|
SETD.0 Bad
|
||||||
|
SETD.3 Bad
|
||||||
|
RETI
|
||||||
|
|
||||||
|
printDP0:
|
||||||
|
LDA.0
|
||||||
|
BRA done0
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI printDP0
|
||||||
|
done0:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
printDP3:
|
||||||
|
LDA.3
|
||||||
|
BRA done3
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.3
|
||||||
|
BRI printDP3
|
||||||
|
done3:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Good:
|
||||||
|
"good"
|
||||||
|
Bad:
|
||||||
|
"bad"
|
||||||
|
After:
|
||||||
|
"AFTER"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot realStart
|
||||||
|
stampTrap stampRegisters
|
||||||
@@ -22,6 +22,7 @@ cd SplitBit-Emulator
|
|||||||
```
|
```
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
The sources are ISO C, and build clean under -std=c11 -pedantic with -Wall -Wextra. Beyond ISO C they need POSIX.1-2008, which the makefile asks for by name, and getopt_long for the long form of the command line options.
|
||||||
3) Assemble a program:
|
3) Assemble a program:
|
||||||
```
|
```
|
||||||
./Assembler Programs/hello.asm
|
./Assembler Programs/hello.asm
|
||||||
@@ -42,6 +43,9 @@ make
|
|||||||
- -f, --fast: Run as fast as the host machine allows, ignoring the emulated cycle rate.
|
- -f, --fast: Run as fast as the host machine allows, ignoring the emulated cycle rate.
|
||||||
- -h, --help: Show help and usage information.
|
- -h, --help: Show help and usage information.
|
||||||
|
|
||||||
|
#### Notes:
|
||||||
|
- If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
|
||||||
|
|
||||||
### Usage:
|
### Usage:
|
||||||
```
|
```
|
||||||
./Assembler [options] [assembly file]
|
./Assembler [options] [assembly file]
|
||||||
@@ -88,6 +92,12 @@ Programs are built inside Tests/build, so running the suite never overwrites the
|
|||||||
./Tests/run.sh hello 8bitFibonacci
|
./Tests/run.sh hello 8bitFibonacci
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To rebuild both tools with the address and undefined behaviour sanitizers and run the suite under them:
|
||||||
|
```
|
||||||
|
make sanitize
|
||||||
|
```
|
||||||
|
This catches reads and writes past the end of an array, use after free, leaks, and undefined arithmetic. It also fills fresh allocations with a junk pattern, which turns a read of uninitialised memory from something that quietly works into something the tests notice. It takes about twice as long as make test, and puts the ordinary binaries back when it finishes.
|
||||||
|
|
||||||
### Additional Info:
|
### Additional Info:
|
||||||
For more information on the custom ISA and programming for SplitBit, see the Programming Manual and Assembler Manual.
|
For more information on the custom ISA and programming for SplitBit, see the Programming Manual and Assembler Manual.
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
|
|||||||
// Free the list of labels.
|
// Free the list of labels.
|
||||||
freeLabelList();
|
freeLabelList();
|
||||||
|
|
||||||
|
// Free the list of vectors.
|
||||||
|
freeVectorList();
|
||||||
|
|
||||||
// Free the output file name
|
// Free the output file name
|
||||||
free(outputFileName);
|
free(outputFileName);
|
||||||
}
|
}
|
||||||
@@ -161,9 +164,11 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate initial space for the intermediate array.
|
// Allocate initial space for the intermediate array. calloc rather than malloc,
|
||||||
|
// because not every element sets every one of its own fields, and a stray
|
||||||
|
// byteLength would quietly shift every address that follows it.
|
||||||
size_t arraySize = 1024;
|
size_t arraySize = 1024;
|
||||||
intermediateElement *intermediateArray = malloc(arraySize * sizeof(intermediateElement));
|
intermediateElement *intermediateArray = calloc(arraySize, sizeof(intermediateElement));
|
||||||
if (!intermediateArray) {
|
if (!intermediateArray) {
|
||||||
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
|
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -176,6 +181,10 @@ int main(int argc, char *argv[]) {
|
|||||||
loadFile(&intermediateArray, source, &index, &arraySize);
|
loadFile(&intermediateArray, source, &index, &arraySize);
|
||||||
populateLabelTable(intermediateArray, index);
|
populateLabelTable(intermediateArray, index);
|
||||||
fillInLabelAddresses(intermediateArray, index);
|
fillInLabelAddresses(intermediateArray, index);
|
||||||
|
// Vectors come after the labels, because a handler is named by its label, and before
|
||||||
|
// the buffers are filled, because SWI needs the number its vector was given.
|
||||||
|
populateVectorTable(intermediateArray, index);
|
||||||
|
fillInVectorReferences(intermediateArray, index);
|
||||||
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
|
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
|
||||||
if (!outputFileName) {
|
if (!outputFileName) {
|
||||||
outputFileName = createOutputFileName(fileName);
|
outputFileName = createOutputFileName(fileName);
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ int checkIfKeyword(intermediateElement *currentElement) {
|
|||||||
} else if (strcmp(currentElement->token, "#Data") == 0) {
|
} else if (strcmp(currentElement->token, "#Data") == 0) {
|
||||||
// Same as for #Program, but mark for inclusion in the Data Segment.
|
// Same as for #Program, but mark for inclusion in the Data Segment.
|
||||||
return KEYWORD_DATA;
|
return KEYWORD_DATA;
|
||||||
|
} else if (strcmp(currentElement->token, "#Align") == 0) {
|
||||||
|
// Puts down as many zero bytes as it takes to reach the next multiple of
|
||||||
|
// the number that follows. The file that needs the boundary is then the
|
||||||
|
// file that asks for it, rather than relying on whatever came before.
|
||||||
|
return KEYWORD_ALIGN;
|
||||||
|
} else if (strcmp(currentElement->token, "#Reserve") == 0) {
|
||||||
|
// Puts down the number of zero bytes that follows, so that a label can
|
||||||
|
// stand for a region rather than just its first byte.
|
||||||
|
return KEYWORD_RESERVE;
|
||||||
|
} else if (strcmp(currentElement->token, "#Vectors") == 0) {
|
||||||
|
// Names which handler belongs to which vector. Nothing here is assembled
|
||||||
|
// into either segment; it is worked out and written into the vector table.
|
||||||
|
return KEYWORD_VECTORS;
|
||||||
} else {
|
} else {
|
||||||
// It's a malformed keyword.
|
// It's a malformed keyword.
|
||||||
fprintf(stderr, RED "Error: Invalid Keyword \"%s\" in file \"%s\" at line number %d.\n" RESET, currentElement->token, currentElement->fileName, currentElement->lineNumber);
|
fprintf(stderr, RED "Error: Invalid Keyword \"%s\" in file \"%s\" at line number %d.\n" RESET, currentElement->token, currentElement->fileName, currentElement->lineNumber);
|
||||||
@@ -159,6 +172,44 @@ int checkIfLiteralValue(intermediateElement *currentElement) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint16_t readCount(intermediateElement *currentElement, const char *what) {
|
||||||
|
const char *token = currentElement->token;
|
||||||
|
int base;
|
||||||
|
const char *baseName;
|
||||||
|
if (token[0] != '0' || (token[1] != 'x' && token[1] != 'd')) {
|
||||||
|
fprintf(stderr, RED "Error: %s needs a number, prefaced with 0x or 0d. Found \"%s\".\n" RESET, what, token);
|
||||||
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (token[1] == 'x') {
|
||||||
|
base = 16;
|
||||||
|
baseName = "hexadecimal";
|
||||||
|
} else {
|
||||||
|
base = 10;
|
||||||
|
baseName = "decimal";
|
||||||
|
}
|
||||||
|
const char *digits = token + 2;
|
||||||
|
if (*digits == '\0') {
|
||||||
|
fprintf(stderr, RED "Error: %s was given \"%s\", which has no digits after its prefix.\n" RESET, what, token);
|
||||||
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
for (const char *c = digits; *c; c++) {
|
||||||
|
if (!(base == 16 ? isxdigit((unsigned char)*c) : isdigit((unsigned char)*c))) {
|
||||||
|
fprintf(stderr, RED "Error: \"%c\" is not a %s digit, in \"%s\".\n" RESET, *c, baseName, token);
|
||||||
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long value = strtol(digits, NULL, base);
|
||||||
|
if (value < 1 || value > 0xFFFF) {
|
||||||
|
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least 1 and no more than 0xFFFF.\n" RESET, what, token);
|
||||||
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return (uint16_t)value;
|
||||||
|
}
|
||||||
|
|
||||||
int checkIfLabel(intermediateElement *currentElement) {
|
int checkIfLabel(intermediateElement *currentElement) {
|
||||||
char *token = currentElement->token;
|
char *token = currentElement->token;
|
||||||
int length = strlen(token);
|
int length = strlen(token);
|
||||||
@@ -207,7 +258,7 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
|
|||||||
// Step 3: Handle string literals
|
// Step 3: Handle string literals
|
||||||
if (c == '"') {
|
if (c == '"') {
|
||||||
while ((c = fgetc(file)) != EOF && c != '"') {
|
while ((c = fgetc(file)) != EOF && c != '"') {
|
||||||
if (i < sizeof(buffer) - 1) {
|
if (i < (int)(sizeof(buffer) - 1)) {
|
||||||
buffer[i++] = c;
|
buffer[i++] = c;
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Error: String literal too long.\n");
|
fprintf(stderr, "Error: String literal too long.\n");
|
||||||
@@ -228,7 +279,7 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
|
|||||||
// Step 4: Handle non-string tokens
|
// Step 4: Handle non-string tokens
|
||||||
ungetc(c, file); // Put the first character back
|
ungetc(c, file); // Put the first character back
|
||||||
while ((c = fgetc(file)) != EOF && !isspace(c) && c != ';') {
|
while ((c = fgetc(file)) != EOF && !isspace(c) && c != ';') {
|
||||||
if (i < sizeof(buffer) - 1) {
|
if (i < (int)(sizeof(buffer) - 1)) {
|
||||||
buffer[i++] = c;
|
buffer[i++] = c;
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Error: Token too long.\n");
|
fprintf(stderr, "Error: Token too long.\n");
|
||||||
|
|||||||
@@ -23,16 +23,31 @@
|
|||||||
#define LABEL_DEFINITION 4
|
#define LABEL_DEFINITION 4
|
||||||
#define VALUE 5
|
#define VALUE 5
|
||||||
#define STRING 6
|
#define STRING 6
|
||||||
|
// A name from the Vector Segment, used as the operand of SWI. It stands for a vector
|
||||||
|
// number rather than an address, so it emits one byte where a label emits two.
|
||||||
|
#define VECTOR_REFERENCE 7
|
||||||
|
// Zero bytes put down to move the cursor along, from #Reserve. How many is known as
|
||||||
|
// soon as it is read.
|
||||||
|
#define PADDING 8
|
||||||
|
// The same, from #Align, where how many depends on where the cursor has got to. The
|
||||||
|
// count is worked out in the second pass and the alignment itself is kept in address.
|
||||||
|
#define ALIGNMENT 9
|
||||||
|
|
||||||
// Keyword values.
|
// Keyword values.
|
||||||
#define KEYWORD_INCLUDE 1
|
#define KEYWORD_INCLUDE 1
|
||||||
#define KEYWORD_PROGRAM 2
|
#define KEYWORD_PROGRAM 2
|
||||||
#define KEYWORD_DATA 3
|
#define KEYWORD_DATA 3
|
||||||
|
#define KEYWORD_VECTORS 4
|
||||||
|
#define KEYWORD_ALIGN 5
|
||||||
|
#define KEYWORD_RESERVE 6
|
||||||
|
|
||||||
// Destination values.
|
// Destination values.
|
||||||
#define NOWHERE 0
|
#define NOWHERE 0
|
||||||
#define PROGRAM 1
|
#define PROGRAM 1
|
||||||
#define DATA 2
|
#define DATA 2
|
||||||
|
// The Vector Segment does not become bytes at an address the way the other two do. It
|
||||||
|
// says which handler belongs to which vector, and the assembler works out the rest.
|
||||||
|
#define VECTORS 3
|
||||||
|
|
||||||
// For colorful text.
|
// For colorful text.
|
||||||
#define RESET "\x1B[0m"
|
#define RESET "\x1B[0m"
|
||||||
@@ -66,4 +81,9 @@ int checkIfLabel(intermediateElement *currentElement);
|
|||||||
|
|
||||||
int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber);
|
int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber);
|
||||||
|
|
||||||
|
// Reads a count written the way a literal is, but allowing the full range of an address
|
||||||
|
// rather than a single byte. #Align and #Reserve both take one, and neither number is
|
||||||
|
// ever emitted as a byte, so there is no reason to hold them to a byte's range.
|
||||||
|
uint16_t readCount(intermediateElement *currentElement, const char *what);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ Instruction instruction_set[] = {
|
|||||||
{0x12, "BRA"},
|
{0x12, "BRA"},
|
||||||
{0x13, "BRB"},
|
{0x13, "BRB"},
|
||||||
{0x14, "BRC"},
|
{0x14, "BRC"},
|
||||||
|
{0x15, "BRD"},
|
||||||
{0x17, "CALL"},
|
{0x17, "CALL"},
|
||||||
|
{0x18, "SWI"},
|
||||||
|
{0x19, "RETI"},
|
||||||
{0x1F, "RET"},
|
{0x1F, "RET"},
|
||||||
// Register Operations:
|
// Register Operations:
|
||||||
{0x20, "RSTA"},
|
{0x20, "RSTA"},
|
||||||
@@ -40,6 +43,10 @@ Instruction instruction_set[] = {
|
|||||||
{0x26, "INIA"},
|
{0x26, "INIA"},
|
||||||
{0x27, "INIB"},
|
{0x27, "INIB"},
|
||||||
{0x28, "CCF"},
|
{0x28, "CCF"},
|
||||||
|
{0x29, "MVQA"},
|
||||||
|
{0x2A, "MVQB"},
|
||||||
|
{0x2B, "SIF"},
|
||||||
|
{0x2C, "CIF"},
|
||||||
// Stack Operations:
|
// Stack Operations:
|
||||||
{0x30, "PSHQ"},
|
{0x30, "PSHQ"},
|
||||||
{0x31, "PSHA"},
|
{0x31, "PSHA"},
|
||||||
@@ -61,6 +68,7 @@ Instruction instruction_set[] = {
|
|||||||
{0x49, "DPDN"},
|
{0x49, "DPDN"},
|
||||||
{0x4A, "LDD"},
|
{0x4A, "LDD"},
|
||||||
{0x4B, "STD"},
|
{0x4B, "STD"},
|
||||||
|
{0x4C, "MVSD"},
|
||||||
// Output Operations:
|
// Output Operations:
|
||||||
{0xD0, "OUTQ"},
|
{0xD0, "OUTQ"},
|
||||||
{0xD1, "OUTA"},
|
{0xD1, "OUTA"},
|
||||||
@@ -92,6 +100,7 @@ int dataPointerOperands(uint8_t opcode) {
|
|||||||
case 0x4A: // LDD
|
case 0x4A: // LDD
|
||||||
case 0x4B: // STD
|
case 0x4B: // STD
|
||||||
return 2;
|
return 2;
|
||||||
|
case 0x15: // BRD
|
||||||
case 0x33: // PSHD
|
case 0x33: // PSHD
|
||||||
case 0x36: // POPD
|
case 0x36: // POPD
|
||||||
case 0x40: // INCD
|
case 0x40: // INCD
|
||||||
@@ -104,6 +113,7 @@ int dataPointerOperands(uint8_t opcode) {
|
|||||||
case 0x47: // SETD
|
case 0x47: // SETD
|
||||||
case 0x48: // DPUP
|
case 0x48: // DPUP
|
||||||
case 0x49: // DPDN
|
case 0x49: // DPDN
|
||||||
|
case 0x4C: // MVSD
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -23,18 +23,62 @@
|
|||||||
// .. 3 "DAT"
|
// .. 3 "DAT"
|
||||||
// .. 2 Data Segment length
|
// .. 2 Data Segment length
|
||||||
// .. M Data Segment
|
// .. M Data Segment
|
||||||
|
// .. 3 "VEC", optional
|
||||||
|
// .. 2 Vector Segment length, in bytes
|
||||||
|
// .. K Vector Segment, four bytes per entry
|
||||||
|
//
|
||||||
|
// The Vector Segment is optional and comes last, so a binary written before it existed
|
||||||
|
// simply ends after its Data Segment and still loads. Each entry is two bytes saying
|
||||||
|
// where in Program Memory the vector sits, then two bytes saying where its handler is,
|
||||||
|
// most significant byte first. It is a list rather than an image of the table, so a
|
||||||
|
// program with three handlers costs twelve bytes instead of a padded kilobyte.
|
||||||
//
|
//
|
||||||
// The feature flags are how a binary says it needs something the base machine does
|
// The feature flags are how a binary says it needs something the base machine does
|
||||||
// not provide, so that an emulator which cannot provide it refuses to run the binary
|
// not provide, so that an emulator which cannot provide it refuses to run the binary
|
||||||
// rather than quietly doing the wrong thing. No features are defined yet; the field
|
// rather than quietly doing the wrong thing. No features are defined yet; the field
|
||||||
// is here so that adding one later does not need another format version.
|
// is here so that adding one later does not need another format version.
|
||||||
|
|
||||||
|
// ---- The vector table ----
|
||||||
|
//
|
||||||
|
// The top kilobyte of Program Memory is reserved for vectors. Both tools have to
|
||||||
|
// agree on where it begins: the CPU starts execution through it, and the assembler
|
||||||
|
// has to refuse program text that would run into it.
|
||||||
|
//
|
||||||
|
// Entries are two bytes each, most significant byte first, the same order the branch
|
||||||
|
// instructions and this file format already use.
|
||||||
|
//
|
||||||
|
// 0xFC00 Software vectors 0 to 255
|
||||||
|
// 0xFE00 Hardware vectors 0 to 255, one for each I/O port
|
||||||
|
//
|
||||||
|
// Software vectors 0 and 1 are start addresses rather than handlers. Vector 0 is
|
||||||
|
// where the machine begins at power on and vector 1 is a warm restart, so a zero in
|
||||||
|
// either of them is not "nothing installed" but the address 0x0000, which is where a
|
||||||
|
// program carrying no vector table of its own begins. A zero in any other entry does
|
||||||
|
// mean no handler is installed, and dispatching through one is a fault.
|
||||||
|
|
||||||
|
#define SOFTWARE_VECTOR_BASE 0xFC00
|
||||||
|
#define HARDWARE_VECTOR_BASE 0xFE00
|
||||||
|
#define VECTOR_ENTRY_BYTES 2
|
||||||
|
#define VECTOR_BOOT 0
|
||||||
|
#define VECTOR_SOFT_RESET 1
|
||||||
|
#define VECTOR_INVALID_OPCODE 2
|
||||||
|
// Vectors 3 to 15 are held back for faults that do not exist yet, so that each cause
|
||||||
|
// can have an entry of its own rather than sharing one and needing a cause register to
|
||||||
|
// tell them apart. Everything from 16 up belongs to programs.
|
||||||
|
#define VECTOR_FIRST_FREE 16
|
||||||
|
|
||||||
|
// The first address the vector table occupies, and so the first address that program
|
||||||
|
// text may not use.
|
||||||
|
#define PROGRAM_TEXT_LIMIT SOFTWARE_VECTOR_BASE
|
||||||
|
|
||||||
#define SPLITBIT_MAGIC "SPBT"
|
#define SPLITBIT_MAGIC "SPBT"
|
||||||
#define SPLITBIT_MAGIC_LENGTH 4
|
#define SPLITBIT_MAGIC_LENGTH 4
|
||||||
#define SPLITBIT_FORMAT_VERSION 1
|
#define SPLITBIT_FORMAT_VERSION 1
|
||||||
#define SPLITBIT_FLAGS_LENGTH 4
|
#define SPLITBIT_FLAGS_LENGTH 4
|
||||||
#define SEGMENT_MARKER_LENGTH 3
|
#define SEGMENT_MARKER_LENGTH 3
|
||||||
#define SEGMENT_LENGTH_BYTES 2
|
#define SEGMENT_LENGTH_BYTES 2
|
||||||
|
// Where a vector sits, and where its handler is.
|
||||||
|
#define VECTOR_ENTRY_FILE_BYTES 4
|
||||||
|
|
||||||
// Everything the format costs a file, on top of the two segments themselves.
|
// Everything the format costs a file, on top of the two segments themselves.
|
||||||
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
|
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
|
||||||
|
|||||||
@@ -164,13 +164,22 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
|||||||
}
|
}
|
||||||
// Read off tokens.
|
// Read off tokens.
|
||||||
while (readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
while (readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||||
if (*intermediateIndex >= *arraySize - 1) {
|
if ((size_t)*intermediateIndex >= *arraySize - 1) {
|
||||||
*arraySize *= 2; // Double the size of the array
|
size_t grownSize = *arraySize * 2; // Double the size of the array.
|
||||||
*intermediateArray = realloc(*intermediateArray, *arraySize * sizeof(intermediateElement));
|
// Into a temporary, so that the old allocation is still ours to free if
|
||||||
if (!intermediateArray) {
|
// this fails, rather than being lost the moment realloc returns NULL.
|
||||||
|
intermediateElement *grown = realloc(*intermediateArray, grownSize * sizeof(intermediateElement));
|
||||||
|
if (!grown) {
|
||||||
fprintf(stderr, RED "Error: Memory reallocation failed.\n" RESET);
|
fprintf(stderr, RED "Error: Memory reallocation failed.\n" RESET);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
// New elements have to start blank. realloc leaves the new space holding
|
||||||
|
// whatever the heap had in it before, and an element that never sets its
|
||||||
|
// own byteLength, such as a keyword or a label definition, would then add
|
||||||
|
// rubbish to the running address and move everything after it.
|
||||||
|
memset(grown + *arraySize, 0, (grownSize - *arraySize) * sizeof(intermediateElement));
|
||||||
|
*intermediateArray = grown;
|
||||||
|
*arraySize = grownSize;
|
||||||
}
|
}
|
||||||
//printf("Token number %d\n", intermediateIndex);
|
//printf("Token number %d\n", intermediateIndex);
|
||||||
// Go ahead and mark what we already know about this token.
|
// Go ahead and mark what we already know about this token.
|
||||||
@@ -185,13 +194,24 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
|||||||
status = NOWHERE;
|
status = NOWHERE;
|
||||||
// Get the filename and work out where it actually is.
|
// Get the filename and work out where it actually is.
|
||||||
(*intermediateIndex)++;
|
(*intermediateIndex)++;
|
||||||
readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber);
|
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||||
|
// The file ended straight after the keyword, so there is no
|
||||||
|
// name to read and nothing sensible to go looking for.
|
||||||
|
fprintf(stderr, RED "Error: #Include without a file name.\n" RESET);
|
||||||
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
char *requested = (*intermediateArray)[*intermediateIndex].token;
|
char *requested = (*intermediateArray)[*intermediateIndex].token;
|
||||||
char *resolved = resolveInclude(fileName, requested);
|
char *resolved = resolveInclude(fileName, requested);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
reportMissingInclude(fileName, requested, lineNumber);
|
reportMissingInclude(fileName, requested, lineNumber);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
// The included file's first token is about to be read into this
|
||||||
|
// same slot, so let the file name go now. Leaving it would strand
|
||||||
|
// the only pointer to it the moment it is overwritten.
|
||||||
|
free((*intermediateArray)[*intermediateIndex].token);
|
||||||
|
(*intermediateArray)[*intermediateIndex].token = NULL;
|
||||||
// recordSourceFile takes the path, and hands back NULL if this file
|
// recordSourceFile takes the path, and hands back NULL if this file
|
||||||
// has already been assembled. Including it twice is harmless, which
|
// has already been assembled. Including it twice is harmless, which
|
||||||
// is what lets two libraries depend on a third.
|
// is what lets two libraries depend on a third.
|
||||||
@@ -209,6 +229,56 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
|||||||
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
|
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
|
||||||
status = DATA;
|
status = DATA;
|
||||||
break;
|
break;
|
||||||
|
case KEYWORD_ALIGN:
|
||||||
|
case KEYWORD_RESERVE: {
|
||||||
|
// Both take a count, and both only make sense somewhere that has a
|
||||||
|
// cursor to move along.
|
||||||
|
const char *what = (testValue == KEYWORD_ALIGN) ? "#Align" : "#Reserve";
|
||||||
|
if (status != PROGRAM && status != DATA) {
|
||||||
|
fprintf(stderr, RED "Error: %s outside the Program or Data Segment.\n There is nothing there for it to move along.\n" RESET, what);
|
||||||
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
// The count is read here rather than being left to the literal check,
|
||||||
|
// because it is an instruction to the assembler and never becomes a
|
||||||
|
// byte, so a byte's range would be the wrong limit for it. A page
|
||||||
|
// alignment needs 256, and a reservation is often far larger.
|
||||||
|
intermediateElement *directive = &(*intermediateArray)[*intermediateIndex];
|
||||||
|
(*intermediateIndex)++;
|
||||||
|
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||||
|
fprintf(stderr, RED "Error: %s without a number.\n" RESET, what);
|
||||||
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
||||||
|
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
||||||
|
uint16_t count = readCount(&(*intermediateArray)[*intermediateIndex], what);
|
||||||
|
|
||||||
|
// The count token itself contributes nothing; the directive carries
|
||||||
|
// everything, so that one element stands for one run of zeroes.
|
||||||
|
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
|
||||||
|
(*intermediateArray)[*intermediateIndex].byteLength = 0;
|
||||||
|
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
|
||||||
|
|
||||||
|
directive->destination = status;
|
||||||
|
if (testValue == KEYWORD_ALIGN) {
|
||||||
|
// How many zeroes this comes to depends on where the cursor has
|
||||||
|
// reached, which is not known until the second pass walks it.
|
||||||
|
directive->type = ALIGNMENT;
|
||||||
|
directive->address = count;
|
||||||
|
directive->byteLength = 0;
|
||||||
|
} else {
|
||||||
|
directive->type = PADDING;
|
||||||
|
directive->byteLength = count;
|
||||||
|
}
|
||||||
|
(*intermediateIndex)++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case KEYWORD_VECTORS:
|
||||||
|
// Set the state to VECTORS. Tokens from here on name handlers rather
|
||||||
|
// than becoming bytes, and the second pass reads them.
|
||||||
|
status = VECTORS;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Next, check to see if it's an instruction.
|
// Next, check to see if it's an instruction.
|
||||||
} else if (checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
|
} else if (checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
|
||||||
@@ -235,6 +305,18 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
|||||||
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
// A name written after SWI is a vector rather than an address, so it
|
||||||
|
// stands for one byte instead of two. This is settled by what the name
|
||||||
|
// follows, so that it does not depend on the Vector Segment having been
|
||||||
|
// read first, which it may not have been: it can live in another file.
|
||||||
|
if (status == PROGRAM
|
||||||
|
&& (*intermediateArray)[*intermediateIndex].type == LABEL
|
||||||
|
&& *intermediateIndex > 0
|
||||||
|
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
|
||||||
|
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x18) {
|
||||||
|
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
|
||||||
|
(*intermediateArray)[*intermediateIndex].byteLength = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(*intermediateArray)[*intermediateIndex].destination = status;
|
(*intermediateArray)[*intermediateIndex].destination = status;
|
||||||
|
|||||||
+267
-10
@@ -29,7 +29,7 @@ void freeLabelList() {
|
|||||||
labelCount = 0;
|
labelCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addLabel(char *labelName, uint16_t address, int type) {
|
void addLabel(char *labelName, uint16_t address, int type, const char *fileName, int lineNumber) {
|
||||||
if (labelCount < MAX_LABELS) {
|
if (labelCount < MAX_LABELS) {
|
||||||
// Duplicate labelName and remove the trailing colon, if present
|
// Duplicate labelName and remove the trailing colon, if present
|
||||||
char *cleanedLabel = strdup(labelName);
|
char *cleanedLabel = strdup(labelName);
|
||||||
@@ -38,6 +38,18 @@ void addLabel(char *labelName, uint16_t address, int type) {
|
|||||||
cleanedLabel[len - 1] = '\0'; // Remove the colon
|
cleanedLabel[len - 1] = '\0'; // Remove the colon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A name may only be defined once. Without this check a reference quietly
|
||||||
|
// resolves to whichever definition came first, so a typo or a name that two
|
||||||
|
// libraries both happen to use is very hard to track down.
|
||||||
|
for (int i = 0; i < labelCount; i++) {
|
||||||
|
if (strcmp(labelArray[i].label, cleanedLabel) == 0) {
|
||||||
|
fprintf(stderr, RED "Error: Label \"%s\" is defined more than once.\n" RESET, cleanedLabel);
|
||||||
|
printf("File: %s at line %d.\n", fileName, lineNumber);
|
||||||
|
free(cleanedLabel);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
labelArray[labelCount].label = cleanedLabel;
|
labelArray[labelCount].label = cleanedLabel;
|
||||||
labelArray[labelCount].address = address;
|
labelArray[labelCount].address = address;
|
||||||
labelArray[labelCount].type = type;
|
labelArray[labelCount].type = type;
|
||||||
@@ -54,11 +66,22 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
|||||||
int dataCount = 0;
|
int dataCount = 0;
|
||||||
// Loop through the array, if there's a label definition, add it to the label list.
|
// Loop through the array, if there's a label definition, add it to the label list.
|
||||||
for (int i = 0; i < arraySize ; i++) {
|
for (int i = 0; i < arraySize ; i++) {
|
||||||
if (intermediateArray[i].type == LABEL_DEFINITION) {
|
// How many zeroes an #Align comes to depends on where the cursor has reached,
|
||||||
|
// so it can only be worked out here, walking the tokens in order. It has to be
|
||||||
|
// settled before the running count moves past it, or every label after it lands
|
||||||
|
// in the wrong place.
|
||||||
|
if (intermediateArray[i].type == ALIGNMENT) {
|
||||||
|
int cursor = (intermediateArray[i].destination == PROGRAM) ? programCount : dataCount;
|
||||||
|
int alignment = intermediateArray[i].address;
|
||||||
|
intermediateArray[i].byteLength = (alignment - (cursor % alignment)) % alignment;
|
||||||
|
}
|
||||||
|
if (intermediateArray[i].type == LABEL_DEFINITION && intermediateArray[i].destination != VECTORS) {
|
||||||
if (intermediateArray[i].destination == PROGRAM) {
|
if (intermediateArray[i].destination == PROGRAM) {
|
||||||
addLabel(intermediateArray[i].token, (uint16_t)programCount, PROGRAM);
|
addLabel(intermediateArray[i].token, (uint16_t)programCount, PROGRAM,
|
||||||
|
intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
||||||
} else {
|
} else {
|
||||||
addLabel(intermediateArray[i].token, (uint16_t)dataCount, DATA);
|
addLabel(intermediateArray[i].token, (uint16_t)dataCount, DATA,
|
||||||
|
intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (intermediateArray[i].destination == PROGRAM) {
|
if (intermediateArray[i].destination == PROGRAM) {
|
||||||
@@ -69,8 +92,10 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
|||||||
if (debugSecondPass) printf("Token: %s with byte length %d to destination %d of type %d\n", intermediateArray[i].token ,intermediateArray[i].byteLength, intermediateArray[i].destination, intermediateArray[i].type);
|
if (debugSecondPass) printf("Token: %s with byte length %d to destination %d of type %d\n", intermediateArray[i].token ,intermediateArray[i].byteLength, intermediateArray[i].destination, intermediateArray[i].type);
|
||||||
|
|
||||||
}
|
}
|
||||||
if (programCount > 0xFFFF ) {
|
if (programCount > PROGRAM_TEXT_LIMIT ) {
|
||||||
fprintf(stderr, RED "Error: Program is too long to fit in Program Memory.\n" RESET);
|
fprintf(stderr, RED "Error: Program is too long to fit in Program Memory.\n"
|
||||||
|
" Program text may not run past 0x%04X, where the vector table begins.\n" RESET,
|
||||||
|
PROGRAM_TEXT_LIMIT - 1);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
if (dataCount > 0xFFFF ) {
|
if (dataCount > 0xFFFF ) {
|
||||||
@@ -79,6 +104,182 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- The Vector Segment ----
|
||||||
|
|
||||||
|
int findLabelAddress(const char *labelName);
|
||||||
|
|
||||||
|
VectorEntry vectorArray[MAX_VECTORS];
|
||||||
|
int vectorArrayCount = 0;
|
||||||
|
|
||||||
|
int vectorCount() {
|
||||||
|
return vectorArrayCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeVectorList() {
|
||||||
|
for (int i = 0; i < vectorArrayCount; i++) {
|
||||||
|
if (vectorArray[i].name) {
|
||||||
|
free(vectorArray[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vectorArrayCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The words the Vector Segment understands. These are spelled without regard to case,
|
||||||
|
// the way mnemonics are, because they are part of the language rather than names the
|
||||||
|
// programmer chose.
|
||||||
|
static int sameWord(const char *a, const char *b) {
|
||||||
|
while (*a && *b) {
|
||||||
|
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
a++;
|
||||||
|
b++;
|
||||||
|
}
|
||||||
|
return *a == *b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The vectors that already mean something. Everything else a program names is numbered
|
||||||
|
// for it, starting above the range held back for faults.
|
||||||
|
static const struct {
|
||||||
|
const char *name;
|
||||||
|
uint8_t index;
|
||||||
|
} reservedVectors[] = {
|
||||||
|
{ "Boot", VECTOR_BOOT },
|
||||||
|
{ "SoftReset", VECTOR_SOFT_RESET },
|
||||||
|
{ "BadOpcode", VECTOR_INVALID_OPCODE },
|
||||||
|
};
|
||||||
|
static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0]));
|
||||||
|
|
||||||
|
static void vectorError(const char *message, intermediateElement *element) {
|
||||||
|
fprintf(stderr, RED "Error: %s\n" RESET, message);
|
||||||
|
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
||||||
|
printf("Token: %s\n", element->token);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next token belonging to the Vector Segment, or -1 if the segment has run out.
|
||||||
|
static int nextVectorToken(intermediateElement *intermediateArray, int arraySize, int from) {
|
||||||
|
for (int i = from; i < arraySize; i++) {
|
||||||
|
if (intermediateArray[i].destination == VECTORS && intermediateArray[i].type != KEYWORD) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler, intermediateElement *element) {
|
||||||
|
if (vectorArrayCount >= MAX_VECTORS) {
|
||||||
|
vectorError("Too many vectors defined.", element);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < vectorArrayCount; i++) {
|
||||||
|
if (vectorArray[i].index == index && vectorArray[i].base == base) {
|
||||||
|
fprintf(stderr, RED "Error: That vector already has a handler.\n" RESET);
|
||||||
|
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (name && vectorArray[i].name && strcmp(vectorArray[i].name, name) == 0) {
|
||||||
|
fprintf(stderr, RED "Error: Vector \"%s\" is named more than once.\n" RESET, name);
|
||||||
|
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vectorArray[vectorArrayCount].name = name ? strdup(name) : NULL;
|
||||||
|
vectorArray[vectorArrayCount].index = index;
|
||||||
|
vectorArray[vectorArrayCount].base = base;
|
||||||
|
vectorArray[vectorArrayCount].handler = handler;
|
||||||
|
vectorArrayCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves the handler named by the token at the given index.
|
||||||
|
static uint16_t resolveHandler(intermediateElement *intermediateArray, int at, const char *what) {
|
||||||
|
if (at < 0) {
|
||||||
|
fprintf(stderr, RED "Error: %s is not followed by a handler to go to.\n" RESET, what);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (intermediateArray[at].type != LABEL) {
|
||||||
|
vectorError("A vector's handler has to be named by a label.", &intermediateArray[at]);
|
||||||
|
}
|
||||||
|
int address = findLabelAddress(intermediateArray[at].token);
|
||||||
|
if (address == -1) {
|
||||||
|
vectorError("That handler does not exist.", &intermediateArray[at]);
|
||||||
|
}
|
||||||
|
return (uint16_t)address;
|
||||||
|
}
|
||||||
|
|
||||||
|
void populateVectorTable(intermediateElement *intermediateArray, int arraySize) {
|
||||||
|
// Software vectors a program names for itself are numbered in the order they are
|
||||||
|
// written, starting above the block held back for faults. A programmer never types
|
||||||
|
// one, so there is no way to land on a reserved vector by accident.
|
||||||
|
int nextFreeVector = VECTOR_FIRST_FREE;
|
||||||
|
|
||||||
|
int i = nextVectorToken(intermediateArray, arraySize, 0);
|
||||||
|
while (i >= 0) {
|
||||||
|
char *token = intermediateArray[i].token;
|
||||||
|
|
||||||
|
if (sameWord(token, "Device")) {
|
||||||
|
// A device is named by the port it is plugged into, because that is what
|
||||||
|
// decides which vector it arrives through. There is nothing to allocate.
|
||||||
|
int portToken = nextVectorToken(intermediateArray, arraySize, i + 1);
|
||||||
|
if (portToken < 0 || intermediateArray[portToken].type != VALUE) {
|
||||||
|
vectorError("Device has to say which port, as a number.", &intermediateArray[i]);
|
||||||
|
}
|
||||||
|
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
|
||||||
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
|
||||||
|
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
|
||||||
|
handler, &intermediateArray[i]);
|
||||||
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t index;
|
||||||
|
int reserved = 0;
|
||||||
|
for (int r = 0; r < reservedVectorCount; r++) {
|
||||||
|
if (sameWord(token, reservedVectors[r].name)) {
|
||||||
|
index = reservedVectors[r].index;
|
||||||
|
reserved = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!reserved) {
|
||||||
|
if (nextFreeVector > 255) {
|
||||||
|
vectorError("There are no software vectors left to give this one.", &intermediateArray[i]);
|
||||||
|
}
|
||||||
|
index = (uint8_t)nextFreeVector;
|
||||||
|
nextFreeVector++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
|
||||||
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
|
||||||
|
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, &intermediateArray[i]);
|
||||||
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillInVectorReferences(intermediateElement *intermediateArray, int arraySize) {
|
||||||
|
for (int i = 0; i < arraySize; i++) {
|
||||||
|
if (intermediateArray[i].type != VECTOR_REFERENCE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int found = 0;
|
||||||
|
for (int v = 0; v < vectorArrayCount; v++) {
|
||||||
|
if (vectorArray[v].name && strcmp(vectorArray[v].name, intermediateArray[i].token) == 0) {
|
||||||
|
if (vectorArray[v].base != SOFTWARE_VECTOR_BASE) {
|
||||||
|
vectorError("SWI can only reach a software vector.", &intermediateArray[i]);
|
||||||
|
}
|
||||||
|
intermediateArray[i].byteValue = vectorArray[v].index;
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, RED "Error: \"%s\" is not a vector.\n Names used with SWI have to be given a handler in a #Vectors section.\n" RESET,
|
||||||
|
intermediateArray[i].token);
|
||||||
|
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int findLabelAddress(const char *labelName) {
|
int findLabelAddress(const char *labelName) {
|
||||||
for (int i = 0; i < labelCount; i++) {
|
for (int i = 0; i < labelCount; i++) {
|
||||||
if (strcmp(labelArray[i].label, labelName) == 0) {
|
if (strcmp(labelArray[i].label, labelName) == 0) {
|
||||||
@@ -90,6 +291,12 @@ int findLabelAddress(const char *labelName) {
|
|||||||
|
|
||||||
void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize) {
|
void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize) {
|
||||||
for (int i = 0; i < arraySize; i++) {
|
for (int i = 0; i < arraySize; i++) {
|
||||||
|
// The Vector Segment is resolved separately. Most of what it holds is not a
|
||||||
|
// label at all: the words that name a vector are the segment's own, and looking
|
||||||
|
// them up here would report them as undefined.
|
||||||
|
if (intermediateArray[i].destination == VECTORS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (intermediateArray[i].type == LABEL) {
|
if (intermediateArray[i].type == LABEL) {
|
||||||
// Look up the label in the label table
|
// Look up the label in the label table
|
||||||
int address = findLabelAddress(intermediateArray[i].token);
|
int address = findLabelAddress(intermediateArray[i].token);
|
||||||
@@ -122,12 +329,21 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
|
|||||||
int nextType = nextTokenType(intermediateArray, arraySize, i);
|
int nextType = nextTokenType(intermediateArray, arraySize, i);
|
||||||
const char *problem = NULL;
|
const char *problem = NULL;
|
||||||
|
|
||||||
if (((opcode & 0xF0) == 0x10) && (opcode != 0x1F)) {
|
// Listed rather than matched on the high nibble, because not every instruction in
|
||||||
|
// the branch block takes an address: RET has none, and BRD gets its destination
|
||||||
|
// from a Data Pointer instead of from the program.
|
||||||
|
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 ||
|
||||||
|
opcode == 0x13 || opcode == 0x14 || opcode == 0x17) {
|
||||||
// Branches and CALL take a two byte address, which only a label can supply.
|
// Branches and CALL take a two byte address, which only a label can supply.
|
||||||
if (nextType != LABEL) problem = "Branch without label.";
|
if (nextType != LABEL) problem = "Branch without label.";
|
||||||
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
||||||
// The instruction is either an input or output and must be followed by a value.
|
// The instruction is either an input or output and must be followed by a value.
|
||||||
if (nextType != VALUE) problem = "I/O without destination port.";
|
if (nextType != VALUE) problem = "I/O without destination port.";
|
||||||
|
} else if (opcode == 0x18) {
|
||||||
|
// SWI names a vector, either by the name it was given in the Vector Segment or,
|
||||||
|
// rarely, as a literal number. Without one it swallows whatever follows it and
|
||||||
|
// every address after that shifts.
|
||||||
|
if (nextType != VECTOR_REFERENCE && nextType != VALUE) problem = "SWI without a vector to go to.";
|
||||||
} else if (opcode == 0x26 || opcode == 0x27) {
|
} else if (opcode == 0x26 || opcode == 0x27) {
|
||||||
// INIA and INIB must be followed by the literal value to load.
|
// INIA and INIB must be followed by the literal value to load.
|
||||||
if (nextType != VALUE) problem = "Immediate load without a value to load.";
|
if (nextType != VALUE) problem = "Immediate load without a value to load.";
|
||||||
@@ -172,6 +388,17 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
|
|||||||
// Add literal value to Program buffer.
|
// Add literal value to Program buffer.
|
||||||
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
||||||
break;
|
break;
|
||||||
|
case VECTOR_REFERENCE:
|
||||||
|
// A vector is a number rather than a place, so this is one byte
|
||||||
|
// where a label would be two.
|
||||||
|
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
||||||
|
break;
|
||||||
|
case PADDING:
|
||||||
|
case ALIGNMENT:
|
||||||
|
for (int z = 0; z < intermediateArray[i].byteLength; z++) {
|
||||||
|
Program[(*programCount)++] = 0x00;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case LABEL:
|
case LABEL:
|
||||||
// Split 16-bit label address into high and low bytes.
|
// Split 16-bit label address into high and low bytes.
|
||||||
Program[(*programCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
Program[(*programCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
||||||
@@ -199,6 +426,12 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
|
|||||||
Data[(*dataCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
Data[(*dataCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
||||||
Data[(*dataCount)++] = intermediateArray[i].address & 0xFF; // Low byte
|
Data[(*dataCount)++] = intermediateArray[i].address & 0xFF; // Low byte
|
||||||
break;
|
break;
|
||||||
|
case PADDING:
|
||||||
|
case ALIGNMENT:
|
||||||
|
for (int z = 0; z < intermediateArray[i].byteLength; z++) {
|
||||||
|
Data[(*dataCount)++] = 0x00;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +463,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
|||||||
fputc(programSize & 0xFF, outputFile); // Low byte
|
fputc(programSize & 0xFF, outputFile); // Low byte
|
||||||
|
|
||||||
// Write the Program buffer to the file
|
// Write the Program buffer to the file
|
||||||
if (fwrite(Program, sizeof(uint8_t), programCount, outputFile) != programCount) {
|
if (fwrite(Program, sizeof(uint8_t), programCount, outputFile) != (size_t)programCount) {
|
||||||
fprintf(stderr, RED "Error: Failed to write Program data to file \"%s\".\n" RESET, outputFileName);
|
fprintf(stderr, RED "Error: Failed to write Program data to file \"%s\".\n" RESET, outputFileName);
|
||||||
fclose(outputFile);
|
fclose(outputFile);
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -245,13 +478,37 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
|||||||
fputc(dataSize & 0xFF, outputFile); // Low byte
|
fputc(dataSize & 0xFF, outputFile); // Low byte
|
||||||
|
|
||||||
// Write the Data buffer to the file
|
// Write the Data buffer to the file
|
||||||
if (fwrite(Data, sizeof(uint8_t), dataCount, outputFile) != dataCount) {
|
if (fwrite(Data, sizeof(uint8_t), dataCount, outputFile) != (size_t)dataCount) {
|
||||||
fprintf(stderr, RED "Error: Failed to write Data data to file \"%s\".\n" RESET, outputFileName);
|
fprintf(stderr, RED "Error: Failed to write Data data to file \"%s\".\n" RESET, outputFileName);
|
||||||
fclose(outputFile);
|
fclose(outputFile);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Vector Segment, only if the program named any. Leaving it out entirely is
|
||||||
|
// what lets a binary written before vectors existed still load: the reader treats
|
||||||
|
// the end of the file as an empty table rather than a missing one.
|
||||||
|
int vectorBytes = 0;
|
||||||
|
if (vectorArrayCount > 0) {
|
||||||
|
fwrite("VEC", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
||||||
|
vectorBytes = vectorArrayCount * VECTOR_ENTRY_FILE_BYTES;
|
||||||
|
fputc((vectorBytes >> 8) & 0xFF, outputFile);
|
||||||
|
fputc(vectorBytes & 0xFF, outputFile);
|
||||||
|
for (int i = 0; i < vectorArrayCount; i++) {
|
||||||
|
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
||||||
|
fputc((slot >> 8) & 0xFF, outputFile);
|
||||||
|
fputc(slot & 0xFF, outputFile);
|
||||||
|
fputc((vectorArray[i].handler >> 8) & 0xFF, outputFile);
|
||||||
|
fputc(vectorArray[i].handler & 0xFF, outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fclose(outputFile);
|
fclose(outputFile);
|
||||||
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
|
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
|
||||||
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount + dataCount + SPLITBIT_HEADER_BYTES));
|
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount);
|
||||||
|
if (vectorArrayCount > 0) {
|
||||||
|
printf(GREEN " Vectors: %d.\n" RESET, vectorArrayCount);
|
||||||
|
}
|
||||||
|
printf(GREEN " Total size: %d bytes.\n" RESET,
|
||||||
|
(programCount + dataCount + SPLITBIT_HEADER_BYTES
|
||||||
|
+ (vectorArrayCount > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "Assm-util.h"
|
#include "Assm-util.h"
|
||||||
|
|
||||||
#define MAX_LABELS 256
|
#define MAX_LABELS 256
|
||||||
|
#define MAX_VECTORS 256
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* label;
|
char* label;
|
||||||
@@ -19,8 +20,28 @@ typedef struct {
|
|||||||
int type;
|
int type;
|
||||||
} Label;
|
} Label;
|
||||||
|
|
||||||
|
// One line of the Vector Segment, once it has been worked out.
|
||||||
|
typedef struct {
|
||||||
|
char* name; // What it was called, or NULL for a device, which is named by its port.
|
||||||
|
uint8_t index; // Which vector in its table.
|
||||||
|
uint16_t base; // Which table: software or hardware.
|
||||||
|
uint16_t handler; // Where the handler ended up.
|
||||||
|
} VectorEntry;
|
||||||
|
|
||||||
void freeLabelList();
|
void freeLabelList();
|
||||||
|
|
||||||
|
void freeVectorList();
|
||||||
|
|
||||||
|
// Reads the Vector Segment: allocates a number to every named vector, works out which
|
||||||
|
// vector each device line means, and resolves the handlers. Runs after the labels are
|
||||||
|
// known, because a handler is named by its label.
|
||||||
|
void populateVectorTable(intermediateElement *intermediateArray, int arraySize);
|
||||||
|
|
||||||
|
// Turns each vector name used as an operand of SWI into the number it was given.
|
||||||
|
void fillInVectorReferences(intermediateElement *intermediateArray, int arraySize);
|
||||||
|
|
||||||
|
int vectorCount();
|
||||||
|
|
||||||
void populateLabelTable(intermediateElement *intermediateArray, int arraySize);
|
void populateLabelTable(intermediateElement *intermediateArray, int arraySize);
|
||||||
|
|
||||||
void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize);
|
void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize);
|
||||||
|
|||||||
@@ -101,6 +101,49 @@ static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
|
|||||||
return loadSegment(file, Memory, length);
|
return loadSegment(file, Memory, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reads the Vector Segment, which is optional and last. A file that simply ends here
|
||||||
|
// was written before vectors existed, and an empty table is exactly right for it: every
|
||||||
|
// entry reads as zero, which means no handler, and the boot vector reading zero means
|
||||||
|
// the program starts at 0x0000 the way it always did.
|
||||||
|
//
|
||||||
|
// Each entry says where in Program Memory the vector sits and where its handler is, so
|
||||||
|
// installing one is a write straight into the vector table.
|
||||||
|
static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
|
||||||
|
int first = fgetc(file);
|
||||||
|
if (first == EOF) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ungetc(first, file);
|
||||||
|
|
||||||
|
char found[SEGMENT_MARKER_LENGTH + 1];
|
||||||
|
if (readMarker(file, "VEC", SEGMENT_MARKER_LENGTH, found)) {
|
||||||
|
fprintf(stderr, "Error: Expected a \"VEC\" segment here, found \"%s\".\n", found);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
uint32_t length;
|
||||||
|
if (readNumber(file, SEGMENT_LENGTH_BYTES, "the vector segment length", &length)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (length % VECTOR_ENTRY_FILE_BYTES != 0) {
|
||||||
|
fprintf(stderr, "Error: The vector segment is %u bytes, which is not a whole number of vectors.\n", length);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < length / VECTOR_ENTRY_FILE_BYTES; i++) {
|
||||||
|
uint32_t slot, handler;
|
||||||
|
if (readNumber(file, 2, "a vector address", &slot)
|
||||||
|
|| readNumber(file, 2, "a handler address", &handler)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (slot < SOFTWARE_VECTOR_BASE) {
|
||||||
|
fprintf(stderr, "Error: This binary puts a vector at 0x%04X, which is below the vector table.\n", slot);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Program[slot] = (handler >> 8) & 0xFF;
|
||||||
|
Program[(uint16_t)(slot + 1)] = handler & 0xFF;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
||||||
FILE *file = fopen(path, "rb");
|
FILE *file = fopen(path, "rb");
|
||||||
if (file == NULL) {
|
if (file == NULL) {
|
||||||
@@ -111,7 +154,8 @@ uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
|||||||
// here means there is one exit, and so only one place that has to close the file.
|
// here means there is one exit, and so only one place that has to close the file.
|
||||||
uint8_t failed = readFileHeader(file)
|
uint8_t failed = readFileHeader(file)
|
||||||
|| readSegment(file, "PRG", Program)
|
|| readSegment(file, "PRG", Program)
|
||||||
|| readSegment(file, "DAT", Data);
|
|| readSegment(file, "DAT", Data)
|
||||||
|
|| readVectorSegment(file, Program);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|||||||
+213
-20
@@ -5,15 +5,78 @@
|
|||||||
|
|
||||||
#include "cpu.h"
|
#include "cpu.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
|
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
|
||||||
|
|
||||||
uint16_t shiftRegister;
|
uint16_t shiftRegister;
|
||||||
|
|
||||||
|
// Reads one entry out of a vector table. Most significant byte first, matching the
|
||||||
|
// branch instructions and the binary format.
|
||||||
|
static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {
|
||||||
|
uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES;
|
||||||
|
return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds an interrupt frame and dispatches through a vector. The resume address is the
|
||||||
|
// address execution should carry on from once the handler returns, and it goes into the
|
||||||
|
// frame as a real address so that a handler can read it and make sense of it.
|
||||||
|
//
|
||||||
|
// Returns 0 if it dispatched. If the vector is empty there is nothing to dispatch to, so
|
||||||
|
// it raises a fault and returns 1 rather than jumping to the bottom of Program Memory
|
||||||
|
// and running whatever happens to be there.
|
||||||
|
//
|
||||||
|
// Note that a zero entry means "no handler" to everything that dispatches, including the
|
||||||
|
// two entries the CPU treats as start addresses when it reads them at reset. The
|
||||||
|
// exemption belongs to that one read, not to the entries themselves.
|
||||||
|
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, uint16_t resumeAddress) {
|
||||||
|
uint16_t handler = readVector(cpu->Program, base, index);
|
||||||
|
if (handler == 0x0000) {
|
||||||
|
cpu->Fault = FAULT_NO_HANDLER;
|
||||||
|
cpu->FaultVector = index;
|
||||||
|
cpu->Status |= STATUS_FAULT;
|
||||||
|
cpu->Status |= STATUS_HALT;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data
|
||||||
|
// Pointer first, so that anything walking the Stack sees a familiar shape.
|
||||||
|
cpu->Data[cpu->StackPointer] = resumeAddress & 0xFF;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
cpu->Data[cpu->StackPointer] = (resumeAddress >> 8) & 0xFF;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
||||||
|
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
}
|
||||||
|
cpu->Data[cpu->StackPointer] = cpu->B;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
cpu->Data[cpu->StackPointer] = cpu->A;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
cpu->Data[cpu->StackPointer] = cpu->Q;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
cpu->Data[cpu->StackPointer] = cpu->Status;
|
||||||
|
cpu->StackPointer--;
|
||||||
|
// A handler runs with hardware interrupts held off unless it says otherwise, so an
|
||||||
|
// interrupt cannot arrive inside the handler for another one and grow the Stack
|
||||||
|
// without bound. The old setting rode into the frame inside the Status register, so
|
||||||
|
// RETI puts it back without anything having to remember it separately.
|
||||||
|
cpu->Status &= ~STATUS_INTERRUPT;
|
||||||
|
// The Program Counter is stepped after every instruction, so land one short of the
|
||||||
|
// handler and let that step land on its first byte. genericBranch does the same.
|
||||||
|
cpu->ProgramCounter = handler - 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
|
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
|
||||||
cpu->A = 0;
|
cpu->A = 0;
|
||||||
cpu->B = 0;
|
cpu->B = 0;
|
||||||
cpu->Q = 0;
|
cpu->Q = 0;
|
||||||
cpu->Status = 0;
|
cpu->Status = 0;
|
||||||
cpu->ProgramCounter = 0x0000;
|
// Execution begins wherever the boot vector points. It is a start address rather
|
||||||
|
// than a handler, so a zero there is not "nothing installed" but the address
|
||||||
|
// 0x0000, which is where a program carrying no vector table of its own begins.
|
||||||
|
// That is what lets everything written before the table existed still run.
|
||||||
|
cpu->ProgramCounter = readVector(programMemory, SOFTWARE_VECTOR_BASE, VECTOR_BOOT);
|
||||||
// Every Data Pointer starts at the bottom of Data Memory.
|
// Every Data Pointer starts at the bottom of Data Memory.
|
||||||
for (int i = 0; i < DATA_POINTERS; i++) {
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
||||||
cpu->DataPointer[i] = 0x0000;
|
cpu->DataPointer[i] = 0x0000;
|
||||||
@@ -21,6 +84,8 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
|
|||||||
cpu->StackPointer = 0xFFFF;
|
cpu->StackPointer = 0xFFFF;
|
||||||
cpu->Program = programMemory;
|
cpu->Program = programMemory;
|
||||||
cpu->Data = dataMemory;
|
cpu->Data = dataMemory;
|
||||||
|
cpu->Fault = FAULT_NONE;
|
||||||
|
cpu->FaultVector = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void genericBranch(CPURegisters *cpu){
|
void genericBranch(CPURegisters *cpu){
|
||||||
@@ -70,25 +135,29 @@ uint16_t *selectDataPointer(CPURegisters *cpu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
||||||
|
// ADD and SUB share this. It is declared here rather than after a case label
|
||||||
|
// because a label may only be followed by a statement in ISO C, and a
|
||||||
|
// declaration is not one.
|
||||||
|
uint16_t result;
|
||||||
switch(Instruction) {
|
switch(Instruction) {
|
||||||
// 0x - Arithmetic and Logic Operations.
|
// 0x - Arithmetic and Logic Operations.
|
||||||
case 0x00:
|
case 0x00:
|
||||||
// ADD - A + B + Carry -> Q
|
// ADD - A + B + Carry -> Q
|
||||||
uint16_t result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & 0x01);
|
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
|
||||||
if (result > 255) {
|
if (result > 255) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->Q = result & 0xFF;
|
cpu->Q = result & 0xFF;
|
||||||
break;
|
break;
|
||||||
case 0x01:
|
case 0x01:
|
||||||
// SUB - A - B - Carry -> Q
|
// SUB - A - B - Carry -> Q
|
||||||
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & 0x01);
|
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
|
||||||
if (result > 255) {
|
if (result > 255) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->Q = result & 0xFF;
|
cpu->Q = result & 0xFF;
|
||||||
break;
|
break;
|
||||||
@@ -162,16 +231,68 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
break;
|
break;
|
||||||
case 0x14:
|
case 0x14:
|
||||||
// BRC - Do an immediate branch if the Carry Flag is set.
|
// BRC - Do an immediate branch if the Carry Flag is set.
|
||||||
if (cpu->Status & 0x01) {
|
if (cpu->Status & STATUS_CARRY) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
} else {
|
} else {
|
||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 0x15: {
|
||||||
|
// BRD - Branch to the address held in a Data Pointer.
|
||||||
|
// This is the only branch whose destination is not written into the
|
||||||
|
// program, which is what makes a table of addresses something a program
|
||||||
|
// can dispatch through rather than only read.
|
||||||
|
uint16_t destination = *selectDataPointer(cpu);
|
||||||
|
// stepCPU adds one after every instruction, so aim one short.
|
||||||
|
cpu->ProgramCounter = destination - 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 0x17:
|
case 0x17:
|
||||||
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
|
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
|
||||||
genericCall(cpu);
|
genericCall(cpu);
|
||||||
break;
|
break;
|
||||||
|
case 0x18: {
|
||||||
|
// SWI - Software Interrupt. The byte after the opcode names the vector.
|
||||||
|
// Never masked: this is an instruction the program deliberately ran, not
|
||||||
|
// something a device asked for.
|
||||||
|
uint16_t site = cpu->ProgramCounter;
|
||||||
|
cpu->ProgramCounter++;
|
||||||
|
uint8_t vector = cpu->Program[cpu->ProgramCounter];
|
||||||
|
// Execution resumes after the operand, which the Program Counter is sitting
|
||||||
|
// on, so the resume address is one further on than that.
|
||||||
|
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) {
|
||||||
|
// No handler. Leave the Program Counter on the SWI itself rather than
|
||||||
|
// its operand, so the report names the instruction that failed.
|
||||||
|
cpu->ProgramCounter = site - 1;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case 0x19: {
|
||||||
|
// RETI - Return from an interrupt. Pops the frame in the exact reverse of
|
||||||
|
// the order enterInterrupt pushed it.
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->Status = cpu->Data[cpu->StackPointer];
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->Q = cpu->Data[cpu->StackPointer];
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->A = cpu->Data[cpu->StackPointer];
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->B = cpu->Data[cpu->StackPointer];
|
||||||
|
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer];
|
||||||
|
}
|
||||||
|
uint16_t resumeAddress;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
resumeAddress = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
resumeAddress = resumeAddress | (uint16_t)cpu->Data[cpu->StackPointer];
|
||||||
|
// The frame holds the address to carry on from. The Program Counter is
|
||||||
|
// stepped after every instruction, so land one short of it. RET does the
|
||||||
|
// same job with its +2, for the same reason.
|
||||||
|
cpu->ProgramCounter = resumeAddress - 1;
|
||||||
|
} break;
|
||||||
case 0x1F:
|
case 0x1F:
|
||||||
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
|
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
|
||||||
// Pop A from the Stack.
|
// Pop A from the Stack.
|
||||||
@@ -213,9 +334,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// INCA - Add 1 to A.
|
// INCA - Add 1 to A.
|
||||||
// Set the Carry Flag if the register overflows.
|
// Set the Carry Flag if the register overflows.
|
||||||
if (cpu->A == 0xFF) {
|
if (cpu->A == 0xFF) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->A++;
|
cpu->A++;
|
||||||
break;
|
break;
|
||||||
@@ -223,9 +344,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// INCB - Add 1 to B.
|
// INCB - Add 1 to B.
|
||||||
// Set the Carry Flag if the register overflows.
|
// Set the Carry Flag if the register overflows.
|
||||||
if (cpu->B == 0xFF) {
|
if (cpu->B == 0xFF) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->B++;
|
cpu->B++;
|
||||||
break;
|
break;
|
||||||
@@ -233,9 +354,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// DECA - Subtract 1 from A.
|
// DECA - Subtract 1 from A.
|
||||||
// Set the Carry Flag if the register underflows.
|
// Set the Carry Flag if the register underflows.
|
||||||
if (cpu->A == 0x00) {
|
if (cpu->A == 0x00) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->A--;
|
cpu->A--;
|
||||||
break;
|
break;
|
||||||
@@ -243,9 +364,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// DECB - Subtract 1 from B.
|
// DECB - Subtract 1 from B.
|
||||||
// Set the Carry Flag if the register underflows.
|
// Set the Carry Flag if the register underflows.
|
||||||
if (cpu->B == 0x00) {
|
if (cpu->B == 0x00) {
|
||||||
cpu->Status |= 0x01;
|
cpu->Status |= STATUS_CARRY;
|
||||||
} else {
|
} else {
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
}
|
}
|
||||||
cpu->B--;
|
cpu->B--;
|
||||||
break;
|
break;
|
||||||
@@ -261,7 +382,25 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
break;
|
break;
|
||||||
case 0x28:
|
case 0x28:
|
||||||
// CCF - Clear the Carry Flag.
|
// CCF - Clear the Carry Flag.
|
||||||
cpu->Status &= ~0x01;
|
cpu->Status &= ~STATUS_CARRY;
|
||||||
|
break;
|
||||||
|
case 0x29:
|
||||||
|
// MVQA - Copy Q into A.
|
||||||
|
cpu->A = cpu->Q;
|
||||||
|
break;
|
||||||
|
case 0x2A:
|
||||||
|
// MVQB - Copy Q into B.
|
||||||
|
cpu->B = cpu->Q;
|
||||||
|
break;
|
||||||
|
case 0x2B:
|
||||||
|
// SIF - Set the Interrupt Flag, enabling hardware interrupts.
|
||||||
|
cpu->Status |= STATUS_INTERRUPT;
|
||||||
|
break;
|
||||||
|
case 0x2C:
|
||||||
|
// CIF - Clear the Interrupt Flag, disabling hardware interrupts.
|
||||||
|
// Software interrupts and faults are delivered either way, so this
|
||||||
|
// only ever holds off a device.
|
||||||
|
cpu->Status &= ~STATUS_INTERRUPT;
|
||||||
break;
|
break;
|
||||||
//
|
//
|
||||||
// 3x - Stack Operations:
|
// 3x - Stack Operations:
|
||||||
@@ -390,6 +529,18 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
|
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 0x4C: {
|
||||||
|
// MVSD - Copy the Stack Pointer into the selected Data Pointer.
|
||||||
|
//
|
||||||
|
// The Stack Pointer still cannot be written, so this does not let a program
|
||||||
|
// move the Stack. It lets a program find it, which is what reading anything
|
||||||
|
// already on the Stack requires. An interrupt handler needs this to reach
|
||||||
|
// its own frame, and so does anything that wants to walk back through the
|
||||||
|
// calls that led to where it is.
|
||||||
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
|
*target = cpu->StackPointer;
|
||||||
|
}
|
||||||
|
break;
|
||||||
//
|
//
|
||||||
// Dx - Output Operations:
|
// Dx - Output Operations:
|
||||||
//
|
//
|
||||||
@@ -429,7 +580,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
break;
|
break;
|
||||||
case 0xFF:
|
case 0xFF:
|
||||||
// HALT - Set the Halt Bit of the Status Register.
|
// HALT - Set the Halt Bit of the Status Register.
|
||||||
cpu->Status |= 0x80;
|
cpu->Status |= STATUS_HALT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown Instruction.
|
// Unknown Instruction.
|
||||||
@@ -439,9 +590,51 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stepCPU(CPURegisters *cpu) {
|
void stepCPU(CPURegisters *cpu) {
|
||||||
if (!(cpu->Status & 0x80)) {
|
if (!(cpu->Status & STATUS_HALT)) {
|
||||||
|
// A device asking for attention is answered between instructions and never
|
||||||
|
// inside one, so the address that goes into the frame is always the start of an
|
||||||
|
// instruction and RETI always lands somewhere meaningful.
|
||||||
|
//
|
||||||
|
// A line that is up while the Interrupt Flag is clear stays up. Masking holds a
|
||||||
|
// device off; it does not lose what the device was asking for.
|
||||||
|
if (cpu->Status & STATUS_INTERRUPT) {
|
||||||
|
int port = nextPendingInterrupt();
|
||||||
|
if (port >= 0) {
|
||||||
|
clearInterrupt((uint8_t)port);
|
||||||
|
if (enterInterrupt(cpu, HARDWARE_VECTOR_BASE, (uint8_t)port, cpu->ProgramCounter)) {
|
||||||
|
// The device asked and nobody was listening. enterInterrupt has
|
||||||
|
// already stopped the machine; correct the cause, because the empty
|
||||||
|
// entry is in the hardware table rather than the software one.
|
||||||
|
cpu->Fault = FAULT_NO_DEVICE_HANDLER;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Entering the handler is what this cycle did, so no instruction runs.
|
||||||
|
// The step puts the Program Counter on the handler's first byte, the
|
||||||
|
// same way it does everywhere else.
|
||||||
|
cpu->ProgramCounter++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// The CPU is not halted, so do a cycle.
|
// The CPU is not halted, so do a cycle.
|
||||||
executeOperation(cpu->Program[cpu->ProgramCounter], cpu);
|
if (executeOperation(cpu->Program[cpu->ProgramCounter], cpu)) {
|
||||||
|
// Nothing decodes that byte. Hand it to the fault vector, which gets the
|
||||||
|
// address of the offending byte itself rather than the one after it, so
|
||||||
|
// that a handler can read the byte that failed and say what it was.
|
||||||
|
//
|
||||||
|
// A handler returning with a bare RETI will therefore meet the same byte
|
||||||
|
// again. That is the documented behaviour: resuming past a fault means
|
||||||
|
// deciding where to resume, which is the handler's business and not the
|
||||||
|
// CPU's guess.
|
||||||
|
uint16_t faultingAddress = cpu->ProgramCounter;
|
||||||
|
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, VECTOR_INVALID_OPCODE, faultingAddress)) {
|
||||||
|
// Nothing is installed, so stop where we are. The Program Counter is
|
||||||
|
// still on the offending byte, which is what the report wants. The
|
||||||
|
// cause is the byte, not the empty vector, so say so.
|
||||||
|
cpu->Fault = FAULT_BAD_OPCODE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Dispatched. Fall through, so the step below lands on the handler.
|
||||||
|
}
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,30 @@
|
|||||||
#error "Cannot preserve more Data Pointers than the CPU has."
|
#error "Cannot preserve more Data Pointers than the CPU has."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// The bits of the Status register that mean something.
|
||||||
|
#define STATUS_CARRY 0x01 // An arithmetic result carried out of, or borrowed into, a byte.
|
||||||
|
#define STATUS_FAULT 0x02 // The CPU met a byte it could not decode, and stopped.
|
||||||
|
#define STATUS_INTERRUPT 0x04 // Hardware interrupts are enabled. Nothing reads this yet.
|
||||||
|
#define STATUS_HALT 0x80 // Execution has stopped, either from HALT or from a fault.
|
||||||
|
|
||||||
|
// What an interrupt puts on the Stack: the resume address, every Data Pointer, and
|
||||||
|
// every register the CPU has. The CALL frame leaves Q and DP3 alone, but that is a
|
||||||
|
// convention between a caller and the subroutine it called. An interrupt arrives in
|
||||||
|
// code that never agreed to give anything up, so it saves the lot.
|
||||||
|
#define INTERRUPT_FRAME_BYTES (2 + DATA_POINTERS * 2 + 4)
|
||||||
|
|
||||||
|
// Why the CPU stopped, when the Fault Flag is set. This is not something a program can
|
||||||
|
// read, and it is deliberately not a register: when a handler is installed, the vector
|
||||||
|
// it arrived through already says what happened, which is why the ISA has no fault
|
||||||
|
// cause. This exists for the case where nothing is installed and the machine is dead,
|
||||||
|
// so that whatever examines the wreckage can say something better than "it stopped".
|
||||||
|
typedef enum {
|
||||||
|
FAULT_NONE = 0,
|
||||||
|
FAULT_BAD_OPCODE, // A byte that does not decode to an instruction.
|
||||||
|
FAULT_NO_HANDLER, // Dispatched through a software vector with nothing in it.
|
||||||
|
FAULT_NO_DEVICE_HANDLER // A device interrupted, and its vector was empty.
|
||||||
|
} FaultCause;
|
||||||
|
|
||||||
// The struct containing the CPU registers.
|
// The struct containing the CPU registers.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t A;
|
uint8_t A;
|
||||||
@@ -37,6 +61,9 @@ typedef struct {
|
|||||||
uint16_t StackPointer;
|
uint16_t StackPointer;
|
||||||
uint8_t *Program;
|
uint8_t *Program;
|
||||||
uint8_t *Data;
|
uint8_t *Data;
|
||||||
|
// Set alongside the Fault Flag, and read only by whatever reports the stop.
|
||||||
|
uint8_t Fault; // A FaultCause.
|
||||||
|
uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER.
|
||||||
} CPURegisters;
|
} CPURegisters;
|
||||||
|
|
||||||
uint8_t executeOperation(uint8_t instruction, CPURegisters *cpu);
|
uint8_t executeOperation(uint8_t instruction, CPURegisters *cpu);
|
||||||
|
|||||||
@@ -99,7 +99,12 @@ int main (int argc, char *argv[]) {
|
|||||||
cycle_timer_init(&timer, CYCLE_RATE);
|
cycle_timer_init(&timer, CYCLE_RATE);
|
||||||
|
|
||||||
uint8_t limitReached = 0;
|
uint8_t limitReached = 0;
|
||||||
while (!(cpu.Status & 0x80) && !limitReached) {
|
while (!(cpu.Status & STATUS_HALT) && !limitReached) {
|
||||||
|
if (options.debug) {
|
||||||
|
// Wait before advancing, not after, so that a keypress is what moves the
|
||||||
|
// machine on rather than something that happens once it already has.
|
||||||
|
getchar();
|
||||||
|
}
|
||||||
int cycles;
|
int cycles;
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
// Debug mode advances one instruction per keypress, so the wall clock
|
// Debug mode advances one instruction per keypress, so the wall clock
|
||||||
@@ -113,7 +118,7 @@ int main (int argc, char *argv[]) {
|
|||||||
for (int i = 0; i < cycles; i++) {
|
for (int i = 0; i < cycles; i++) {
|
||||||
stepCPU(&cpu);
|
stepCPU(&cpu);
|
||||||
cycleCount++;
|
cycleCount++;
|
||||||
if (cpu.Status & 0x80) {
|
if (cpu.Status & STATUS_HALT) {
|
||||||
// We've halted.
|
// We've halted.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,13 +128,26 @@ int main (int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
getchar();
|
|
||||||
printRegisters(&cpu, Program, Data);
|
printRegisters(&cpu, Program, Data);
|
||||||
printf("Cycle: %lu\n", cycleCount);
|
printf("Cycle: %lu\n", cycleCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (limitReached) {
|
if (limitReached) {
|
||||||
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
||||||
|
} else if (cpu.Status & STATUS_FAULT) {
|
||||||
|
// The Program Counter is still pointing at whatever the CPU could not get past.
|
||||||
|
printf("Execution halted after %lu cycles.\n", cycleCount);
|
||||||
|
if (cpu.Fault == FAULT_NO_HANDLER) {
|
||||||
|
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
|
||||||
|
cpu.FaultVector, cpu.ProgramCounter);
|
||||||
|
} else if (cpu.Fault == FAULT_NO_DEVICE_HANDLER) {
|
||||||
|
fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n",
|
||||||
|
cpu.FaultVector, cpu.ProgramCounter, cpu.FaultVector);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n",
|
||||||
|
Program[cpu.ProgramCounter], cpu.ProgramCounter);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
printf("Execution halted after %lu cycles.\n", cycleCount);
|
printf("Execution halted after %lu cycles.\n", cycleCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,37 @@
|
|||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// One bit per port, so a device can ask for attention without anything having to poll
|
||||||
|
// it. Eight ports to the byte, low bit first.
|
||||||
|
#define INTERRUPT_LINE_BYTES 32
|
||||||
|
|
||||||
|
static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES];
|
||||||
|
|
||||||
|
void raiseInterrupt(uint8_t port) {
|
||||||
|
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearInterrupt(uint8_t port) {
|
||||||
|
pendingInterrupts[port >> 3] &= (uint8_t)~(1u << (port & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextPendingInterrupt(void) {
|
||||||
|
// Lowest numbered port wins. This is a scan rather than a priority encoder, which
|
||||||
|
// means there is no arbitration to explain and a programmer can work out what
|
||||||
|
// happens next by reading the port numbers.
|
||||||
|
for (int group = 0; group < INTERRUPT_LINE_BYTES; group++) {
|
||||||
|
if (pendingInterrupts[group] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int bit = 0; bit < 8; bit++) {
|
||||||
|
if (pendingInterrupts[group] & (1u << bit)) {
|
||||||
|
return group * 8 + bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
||||||
// This function sends the DataByte to the appropriate place based on the Port Address.
|
// This function sends the DataByte to the appropriate place based on the Port Address.
|
||||||
switch(Address) {
|
switch(Address) {
|
||||||
@@ -15,6 +46,14 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
|||||||
// Later, I'll want to use a buffer for this for performance, probably.
|
// Later, I'll want to use a buffer for this for performance, probably.
|
||||||
putchar(DataByte);
|
putchar(DataByte);
|
||||||
break;
|
break;
|
||||||
|
case 0x10:
|
||||||
|
// A test device, and about the simplest one that can exist: writing to it
|
||||||
|
// puts its own line up. It stands in for the shape a real device has, where
|
||||||
|
// the CPU asks for something and is interrupted once the answer is ready,
|
||||||
|
// with the waiting taken out so that a test runs the same way every time.
|
||||||
|
// The byte written is ignored; only the asking matters.
|
||||||
|
raiseInterrupt(0x10);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Writes to unused Output Ports are ignored.
|
// Writes to unused Output Ports are ignored.
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -13,4 +13,20 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address);
|
|||||||
|
|
||||||
uint8_t InputHandler(uint8_t Address);
|
uint8_t InputHandler(uint8_t Address);
|
||||||
|
|
||||||
|
// ---- Interrupt lines ----
|
||||||
|
//
|
||||||
|
// One line per port. A device puts its line up to ask for attention, and the CPU takes
|
||||||
|
// it down when it answers. Which line a device uses is not a choice: a device on port N
|
||||||
|
// interrupts on N, which is what saves the machine from needing any arbitration.
|
||||||
|
//
|
||||||
|
// These belong to the bus rather than to the CPU. Nothing here is saved in a frame, and
|
||||||
|
// a program cannot read them except by being interrupted.
|
||||||
|
|
||||||
|
void raiseInterrupt(uint8_t port);
|
||||||
|
|
||||||
|
void clearInterrupt(uint8_t port);
|
||||||
|
|
||||||
|
// The lowest numbered port with its line up, or -1 if none of them are.
|
||||||
|
int nextPendingInterrupt(void);
|
||||||
|
|
||||||
#endif // IO_H
|
#endif // IO_H
|
||||||
|
|||||||
@@ -68,11 +68,31 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
return OPTIONS_OK;
|
return OPTIONS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes a byte out as eight binary digits, most significant first. printf's %b is
|
||||||
|
// a recent addition to C and not available everywhere, so this does it by hand.
|
||||||
|
// The buffer must have room for nine characters.
|
||||||
|
static void formatBinary(uint8_t value, char *out) {
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
out[i] = (value & (0x80 >> i)) ? '1' : '0';
|
||||||
|
}
|
||||||
|
out[8] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
|
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
|
||||||
|
char status[9];
|
||||||
|
formatBinary(cpu->Status, status);
|
||||||
printf("***** CPU Registers *****\n");
|
printf("***** CPU Registers *****\n");
|
||||||
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%08b\n", cpu->A, cpu->B, cpu->Q, cpu->Status);
|
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%s\n", cpu->A, cpu->B, cpu->Q, status);
|
||||||
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
|
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
|
||||||
printf(" Data Pointer: 0x%04X Current Data Value: 0x%02X\n", cpu->DataPointer[0], Data[cpu->DataPointer[0]]);
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
||||||
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer, Data[cpu->StackPointer+1], Data[cpu->StackPointer+2]);
|
printf(" Data Pointer %d: 0x%04X Current Data Value: 0x%02X%s\n",
|
||||||
|
i, cpu->DataPointer[i], Data[cpu->DataPointer[i]],
|
||||||
|
i >= PRESERVED_DATA_POINTERS ? " (volatile)" : "");
|
||||||
|
}
|
||||||
|
// The two casts keep these inside Data Memory. The Stack Pointer starts at the
|
||||||
|
// very top, so without them the display would read off the end of the array
|
||||||
|
// before a single byte has been pushed.
|
||||||
|
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer,
|
||||||
|
Data[(uint16_t)(cpu->StackPointer + 1)], Data[(uint16_t)(cpu->StackPointer + 2)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# SplitBit Assembler Manual:
|
# SplitBit Assembler Manual:
|
||||||
|
|
||||||
SplitBit assembly syntax is similar to many other assembler syntaxes. Whitepsace at the start or end of a line is disregarded by the assembler and may be used to make programs more readable to the programmer. The Instruction Mnemonics are listed in the SplitBit Programming Manual, and the assembler is not case sensitve in regard to the mnemonics.
|
SplitBit assembly syntax is similar to many other assembler syntaxes. Whitespace at the start or end of a line is disregarded by the assembler and may be used to make programs more readable to the programmer. The Instruction Mnemonics are listed in the SplitBit Programming Manual, and the assembler is not case sensitive in regard to the mnemonics.
|
||||||
|
|
||||||
A semicolon, ';', denotes the start of a comment, anything beyond it on a line is disregarded by the assembler.
|
A semicolon, ';', denotes the start of a comment, anything beyond it on a line is disregarded by the assembler.
|
||||||
|
|
||||||
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, and #Data.
|
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, #Data, #Vectors, #Align, and #Reserve.
|
||||||
|
|
||||||
|
The first four say what kind of thing follows them. #Align and #Reserve are instructions to the assembler in the middle of a segment, and are described under Moving The Cursor Along.
|
||||||
|
|
||||||
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
|
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
|
||||||
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
|
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
|
||||||
|
SplitBit programs may have a Vector Segment. You define it with the #Vectors Keyword. See The Vector Segment below.
|
||||||
|
|
||||||
## Literal Values:
|
## Literal Values:
|
||||||
|
|
||||||
@@ -15,7 +18,7 @@ Literal values may be defined in a few ways. Numerical values must be within the
|
|||||||
The assembler will accept:
|
The assembler will accept:
|
||||||
- Hexadecimal values prefaced with 0x, eg. 0x00, 0x7F.
|
- Hexadecimal values prefaced with 0x, eg. 0x00, 0x7F.
|
||||||
- Decimal values prefaced with 0d, eg. 0d0, 0d120, 0d255.
|
- Decimal values prefaced with 0d, eg. 0d0, 0d120, 0d255.
|
||||||
- Strings enclosed in double qoutes, eg. "a", "Hello, World!", "It is dark, you are likely to be eaten by a grue."
|
- Strings enclosed in double quotes, eg. "a", "Hello, World!", "It is dark, you are likely to be eaten by a grue."
|
||||||
|
|
||||||
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
|
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ loopStart:
|
|||||||
errorHandler01:
|
errorHandler01:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A name may only be defined once across a program and everything it includes. Defining it twice is an error, because otherwise a reference resolves to whichever definition came first, and a typo or a name that two libraries both happen to use is very hard to track down.
|
||||||
|
|
||||||
A label may be referenced by name, without the colon, to place its two byte address wherever the reference appears.
|
A label may be referenced by name, without the colon, to place its two byte address wherever the reference appears.
|
||||||
|
|
||||||
In the Program Segment that is how the branch instructions and SETD are given somewhere to go. In the Data Segment it writes the address down as data, which is how a table of addresses is built for LDD to walk.
|
In the Program Segment that is how the branch instructions and SETD are given somewhere to go. In the Data Segment it writes the address down as data, which is how a table of addresses is built for LDD to walk.
|
||||||
@@ -78,10 +83,82 @@ Writing a selector on an instruction that does not work through a Data Pointer i
|
|||||||
|
|
||||||
## Instruction Operands:
|
## Instruction Operands:
|
||||||
|
|
||||||
Instructions that read operand bytes out of Program Memory must be followed by those operands. The branch instructions and CALL take a label; SETD takes a label or a pair of literal bytes; INIA, INIB, DPUP, DPDN, and the input and output instructions each take a single literal byte.
|
Instructions that read operand bytes out of Program Memory must be followed by those operands. The branch instructions and CALL take a label; SETD takes a label or a pair of literal bytes; INIA, INIB, DPUP, DPDN, and the input and output instructions each take a single literal byte; SWI takes the name of a vector, or a literal number.
|
||||||
|
|
||||||
|
Leaving an operand off is an error rather than something the assembler works around, because the instruction would otherwise take whatever followed it as the operand and every address after that would shift.
|
||||||
|
|
||||||
Data Pointer selectors do not count as operands here, because they are written on the mnemonic rather than after it.
|
Data Pointer selectors do not count as operands here, because they are written on the mnemonic rather than after it.
|
||||||
|
|
||||||
|
## Moving The Cursor Along:
|
||||||
|
|
||||||
|
Both segments are written from the beginning, and every label stands for wherever the cursor had reached when the assembler met it. Two directives move that cursor without you having to write zeroes by hand.
|
||||||
|
|
||||||
|
`#Align` puts down as many zero bytes as it takes to reach the next multiple of the number that follows it.
|
||||||
|
|
||||||
|
```
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Align 0x100
|
||||||
|
Segment: ; Guaranteed to begin at a page boundary.
|
||||||
|
```
|
||||||
|
|
||||||
|
This matters for code that does address arithmetic on a pointer's low byte and treats the carry out as reaching the end of something. Both prime sieves work that way, and both now ask for the boundary themselves. Before this existed they relied on print.asm padding its data out to a whole page, which worked but put the requirement in a different file from the code that needed it, and quietly charged every other program 253 bytes for it.
|
||||||
|
|
||||||
|
`#Reserve` puts down the number of zero bytes that follows it, so that a label can stand for a whole region rather than just its first byte.
|
||||||
|
|
||||||
|
```
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Buffer:
|
||||||
|
#Reserve 0d256 ; Anything after this begins 256 bytes further on.
|
||||||
|
Next:
|
||||||
|
```
|
||||||
|
|
||||||
|
Without it a label like Buffer is one byte as far as the assembler knows, so a later label lands inside the region and the two quietly overlap.
|
||||||
|
|
||||||
|
Both take a number written the way literals are, prefaced with 0x or 0d, but the number may go up to 0xFFFF rather than being held to a single byte. Neither number is ever emitted, so a byte's range would be the wrong limit: a page alignment needs 256, and a reservation is often much larger.
|
||||||
|
|
||||||
|
Both work in the Program Segment as well as the Data Segment, and both are an error anywhere else, because outside a segment there is no cursor to move.
|
||||||
|
|
||||||
|
## The Vector Segment:
|
||||||
|
|
||||||
|
A vector says where to go when something happens: the machine starting up, a program asking for a service, a device wanting attention, or the CPU meeting a byte it cannot decode. The Vector Segment says which of your routines belongs to which vector, and the assembler works out the rest.
|
||||||
|
|
||||||
|
A program does not need one. Without a Vector Segment a program starts at the beginning and behaves exactly as it always has.
|
||||||
|
|
||||||
|
Every line names a vector and then the label of the routine that handles it.
|
||||||
|
|
||||||
|
```
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot realStart
|
||||||
|
BadOpcode reportFault
|
||||||
|
openFile openFileHandler
|
||||||
|
Device 0x10 diskReady
|
||||||
|
```
|
||||||
|
|
||||||
|
Three names already mean something:
|
||||||
|
|
||||||
|
| Name | Vector |
|
||||||
|
| --- | --- |
|
||||||
|
| Boot | Where the machine begins at power on. Without this a program starts at the beginning of its Program Segment. |
|
||||||
|
| SoftReset | A warm restart. SWI SoftReset is how a program asks for one. |
|
||||||
|
| BadOpcode | The CPU met a byte that is not an instruction. |
|
||||||
|
|
||||||
|
Anything else you name is a software interrupt of your own. You do not choose its number and you never write one: the assembler allocates them in the order they appear, starting above the range held back for faults that do not exist yet. That is the same bargain as labels everywhere else in SplitBit assembly, where you name a thing and let the assembler work out where it went.
|
||||||
|
|
||||||
|
You then use the name as the operand of SWI:
|
||||||
|
|
||||||
|
```
|
||||||
|
SWI openFile
|
||||||
|
```
|
||||||
|
|
||||||
|
A device is different, because its number is not a choice. A device interrupts on the port it is plugged into, so the Device line says which port rather than giving it a name of its own. The port is a literal value, and the routine after it handles that device.
|
||||||
|
|
||||||
|
The assembler will refuse two handlers for the same vector, a name used with SWI that no Vector Segment gives a handler to, and a handler that is not a label.
|
||||||
|
|
||||||
|
Vectors 3 through 15 are held back for faults that have not been defined yet. They have no names, so there is currently no way to write a handler for one, and none is needed: each will be given a name of its own as the fault it stands for is defined. Because you never write a vector number, there is no way to land on one of them by accident either.
|
||||||
|
|
||||||
## Including Other Files:
|
## Including Other Files:
|
||||||
|
|
||||||
The #Include Keyword tells the assembler to load another file to be assembled along with the current file. It is more or less equivalent to copying the contents of the included file into the current file being processed. You simply put the name of the file to include after the keyword.
|
The #Include Keyword tells the assembler to load another file to be assembled along with the current file. It is more or less equivalent to copying the contents of the included file into the current file being processed. You simply put the name of the file to include after the keyword.
|
||||||
@@ -141,7 +218,7 @@ Programs/makefile in this repository builds every program that way, if you would
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
start: ; By uninforced convention, Program Labels start with a lowercase letter.
|
start: ; By unenforced convention, Program Labels start with a lowercase letter.
|
||||||
SETD HelloString ; Set the Data Pointer to the address of the string.
|
SETD HelloString ; Set the Data Pointer to the address of the string.
|
||||||
CALL printString ; Call the string printing subroutine.
|
CALL printString ; Call the string printing subroutine.
|
||||||
HALT ; End the program.
|
HALT ; End the program.
|
||||||
@@ -158,7 +235,7 @@ printString: ; Expects Data Pointer to be set to the beginning of the str
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
HelloString: ; By uninforced convention, Data Labels start with a capital letter.
|
HelloString: ; By unenforced convention, Data Labels start with a capital letter.
|
||||||
"Hello, World!"
|
"Hello, World!"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -197,3 +274,78 @@ Dest:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Remember that DP0, DP1 and DP2 survive a CALL, so a loop like this one can call a subroutine in the middle without losing either pointer. DP3 does not survive, which is what makes it the pointer a subroutine uses to hand an address back.
|
Remember that DP0, DP1 and DP2 survive a CALL, so a loop like this one can call a subroutine in the middle without losing either pointer. DP3 does not survive, which is what makes it the pointer a subroutine uses to hand an address back.
|
||||||
|
|
||||||
|
## An Example Using Interrupts:
|
||||||
|
|
||||||
|
This program installs three handlers and never writes a vector number. The Boot Vector sends the machine somewhere other than the first byte of the program, a trap the program names for itself is reached with SWI, and the test device on port 0x10 is caught when it asks for attention.
|
||||||
|
|
||||||
|
```
|
||||||
|
; Interrupt handling from all three directions.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
CIF ; Hold devices off while we set up.
|
||||||
|
SETD.0 Greeting
|
||||||
|
CALL printString
|
||||||
|
|
||||||
|
SWI announce ; A trap of our own, reached by name.
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x10 ; Ask the test device for attention. Its line goes up.
|
||||||
|
SIF ; Let it through. It is answered before the next instruction.
|
||||||
|
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; A trap. It is entered with a full frame, so it may use any register it likes
|
||||||
|
; without agreeing anything with the code it interrupted.
|
||||||
|
announce:
|
||||||
|
SETD.0 Trapped
|
||||||
|
CALL printString
|
||||||
|
RETI
|
||||||
|
|
||||||
|
; The device handler. Reached because the device sits on port 0x10.
|
||||||
|
deviceReady:
|
||||||
|
SETD.0 Device
|
||||||
|
CALL printString
|
||||||
|
RETI
|
||||||
|
|
||||||
|
; The fault handler. It reports and stops, rather than trying to carry on.
|
||||||
|
reportFault:
|
||||||
|
SETD.0 Broken
|
||||||
|
CALL printString
|
||||||
|
HALT
|
||||||
|
|
||||||
|
printString: ; Expects DP0 to be set to the beginning of the string.
|
||||||
|
LDA.0
|
||||||
|
BRA printDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI printString
|
||||||
|
printDone:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Greeting:
|
||||||
|
"ready"
|
||||||
|
Trapped:
|
||||||
|
"trap"
|
||||||
|
Device:
|
||||||
|
"device"
|
||||||
|
Broken:
|
||||||
|
"bad opcode"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start ; Begin here rather than at the first byte.
|
||||||
|
BadOpcode reportFault
|
||||||
|
announce announce ; A name of our own. The assembler numbers it.
|
||||||
|
Device 0x10 deviceReady ; Named by the port, because that is what decides it.
|
||||||
|
```
|
||||||
|
|
||||||
|
The output is `ready`, `trap`, then `device`.
|
||||||
|
|
||||||
|
Note that a vector name and a routine name are kept apart, so naming both `announce` is allowed. If that reads as confusing, name them differently: nothing requires them to match.
|
||||||
|
|||||||
+126
-10
@@ -16,8 +16,8 @@ It has ten registers:
|
|||||||
- Q is not preserved through subroutine calls. It can be used to pass a one byte result back to the calling routine.
|
- Q is not preserved through subroutine calls. It can be used to pass a one byte result back to the calling routine.
|
||||||
|
|
||||||
- The Program Counter is a 16 bit pointer into the Program Memory.
|
- The Program Counter is a 16 bit pointer into the Program Memory.
|
||||||
- The PC points to the current operation the CPU is executing, it initializes at Program Address 0x0000.
|
- The PC points to the current operation the CPU is executing. It starts at whatever address the Boot Vector holds. See The Vector Table below.
|
||||||
- The PC is only modified by the branch instructions and the CALL and RET instructions. It cannot be directly set by the programmer.
|
- The PC is only modified by the branch instructions, by CALL and RET, by SWI and RETI, and by an interrupt arriving. It cannot be directly set by the programmer.
|
||||||
|
|
||||||
- The Data Pointers (0-3) are 16 bit pointers into the Data Memory.
|
- The Data Pointers (0-3) are 16 bit pointers into the Data Memory.
|
||||||
- A DP points to a byte of data that the CPU can read or write, and each one initializes at Data Address 0x0000.
|
- A DP points to a byte of data that the CPU can read or write, and each one initializes at Data Address 0x0000.
|
||||||
@@ -27,17 +27,121 @@ It has ten registers:
|
|||||||
- Because DP3 is not preserved, a subroutine can use it to pass an address back to the calling routine, in the same way Q passes back a byte. Unlike Q, an address can refer to as much data as you like.
|
- Because DP3 is not preserved, a subroutine can use it to pass an address back to the calling routine, in the same way Q passes back a byte. Unlike Q, an address can refer to as much data as you like.
|
||||||
|
|
||||||
- The Stack Pointer is a 16 bit pointer into the Data Memory.
|
- The Stack Pointer is a 16 bit pointer into the Data Memory.
|
||||||
- The SP points to the current element of the stack, it initializes at location 0xFFFF.
|
- The SP points to the next free slot, not to the last thing pushed. It initializes at location 0xFFFF, so the first push writes there and the byte pushed last always sits one above the SP.
|
||||||
- The SP value is only modified by the push and pop instructions and cannot be set by the programmer.
|
- The SP value is only modified by the push and pop instructions, by CALL and RET, and by an interrupt arriving or returning. It cannot be set by the programmer. MVSD copies it out without moving it.
|
||||||
- The Stack lives in Data Memory, so a Data Pointer can be aimed at it and used to read what is on it.
|
- The Stack lives in Data Memory, so a Data Pointer can be aimed at it and used to read what is on it. MVSD is how a program finds out where to aim.
|
||||||
|
|
||||||
- The Status register is an 8 bit register whose various bits are used as flags. Only two of these flags are used in the current implementation.
|
- The Status register is an 8 bit register whose various bits are used as flags. Only four of these flags are used in the current implementation.
|
||||||
- Bit 0 is the Carry/Borrow Flag. Any arithmetic operation either sets or clears it depending on whether or not the result causes Q to overflow/underflow. It is a 1 if a carry/underflow occurred, and a 0 otherwise. If A or B overflows or underflows from the use of an increment or decrement instruction, this flag will also be set. Non-overflowing increments or decrements will also reset it.
|
- Bit 0 is the Carry/Borrow Flag. Any arithmetic operation either sets or clears it depending on whether or not the result causes Q to overflow/underflow. It is a 1 if a carry/underflow occurred, and a 0 otherwise. If A or B overflows or underflows from the use of an increment or decrement instruction, this flag will also be set. Non-overflowing increments or decrements will also reset it.
|
||||||
- Bit 7 is the Halt Flag. It is set by the HALT instruction.
|
- Bit 1 is the Fault Flag. It is set when the CPU cannot get past something and no handler was installed to deal with it: a byte that is not an instruction, or a dispatch through an empty vector. See Faults below.
|
||||||
|
- Bit 2 is the Interrupt Flag. It is set by SIF and cleared by CIF. While it is set the CPU answers devices asking for attention; while it is clear they wait. Arriving at a handler clears it, and RETI restores it along with the rest of the Status register. See Hardware Interrupts below.
|
||||||
|
- Bit 7 is the Halt Flag. It is set by the HALT instruction, and by a fault.
|
||||||
|
|
||||||
|
## The Vector Table:
|
||||||
|
|
||||||
|
The top kilobyte of Program Memory is reserved for vectors. Each entry is two bytes, most significant byte first, and holds a Program Memory address.
|
||||||
|
|
||||||
|
| Address | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| 0xFC00 | Software vectors 0 to 255 |
|
||||||
|
| 0xFE00 | Hardware vectors 0 to 255, one for each I/O port |
|
||||||
|
|
||||||
|
Program text may not run past 0xFBFF. The assembler refuses to assemble a program that would.
|
||||||
|
|
||||||
|
The software vectors are given out like this:
|
||||||
|
|
||||||
|
| Vector | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | The Boot Vector. Where the machine begins at power on. |
|
||||||
|
| 1 | The Soft Reset Vector. A warm restart. |
|
||||||
|
| 2 | A byte that is not an instruction. |
|
||||||
|
| 3 to 15 | Held back for faults not yet defined. |
|
||||||
|
| 16 and up | A program's own, given out by the assembler in the order they are named. |
|
||||||
|
|
||||||
|
A programmer does not write vector numbers. Handlers are named in the Vector Segment of an assembly file and used by name, the same way every other address in SplitBit is worked out by the assembler rather than typed. See the SplitBit Assembler Manual.
|
||||||
|
|
||||||
|
The table holds two kinds of entry, and they behave differently when they are zero.
|
||||||
|
|
||||||
|
Software vectors 0 and 1 are start addresses rather than handlers. Vector 0 is the Boot Vector: the CPU reads it at power on and begins executing there. Vector 1 is the Soft Reset Vector, for a warm restart. Nothing dispatches through either of them, and 0x0000 is an ordinary address to begin at, so a zero in one of these two means exactly what it says: start at 0x0000.
|
||||||
|
|
||||||
|
That is deliberate, and it is what lets a program that carries no vector table of its own still run. Program Memory reads as zero where nothing was loaded into it, so such a program's Boot Vector reads 0x0000, which is where its first instruction sits.
|
||||||
|
|
||||||
|
The cost of that rule is worth knowing: a machine with neither a Boot Vector nor anything at 0x0000 will start executing zeroes, and 0x00 decodes as ADD, so it will wander instead of stopping. There is no way for the CPU to tell that case apart from a program that genuinely begins at 0x0000.
|
||||||
|
|
||||||
|
Every other entry is a handler. A zero in one of those means no handler is installed, and dispatching through it is a fault rather than a jump to the bottom of memory.
|
||||||
|
|
||||||
|
The exemption for vectors 0 and 1 belongs to that one read the CPU makes at reset, not to the entries themselves. Anything that dispatches treats a zero as no handler, whichever entry it is, so SWI SoftReset through an empty Soft Reset Vector faults like any other. That is what makes SWI SoftReset the way to ask for a warm restart once one has been installed.
|
||||||
|
|
||||||
|
## Interrupts:
|
||||||
|
|
||||||
|
An interrupt is an involuntary transfer of control. A subroutine call is agreed to by the code that makes it, so CALL can leave Q and Data Pointer 3 alone and let a subroutine pass results back through them. An interrupt arrives in code that has never heard of it, where Q and DP3 are ordinary working registers, so it saves everything:
|
||||||
|
|
||||||
|
| Pushed | Bytes |
|
||||||
|
| --- | --- |
|
||||||
|
| The address to resume at | 2 |
|
||||||
|
| Data Pointers 0 through 3 | 8 |
|
||||||
|
| B, then A, then Q, then Status | 4 |
|
||||||
|
|
||||||
|
That is fourteen bytes of Stack per interrupt, and the order matches CALL: least significant byte first, lowest numbered Data Pointer first.
|
||||||
|
|
||||||
|
Entry clears the Interrupt Flag, so a handler runs without being interrupted again unless it sets the flag itself. The old value of the flag rides into the frame inside the Status register, so RETI restores it along with everything else and nothing has to remember it separately.
|
||||||
|
|
||||||
|
RETI pops the frame and carries on from the address in it. The frame holds a real address rather than an adjusted one, so a handler can read it and make sense of where it came from.
|
||||||
|
|
||||||
|
A handler reaches its own frame with MVSD. The Stack Pointer points at the next free slot, so everything in the frame sits above it:
|
||||||
|
|
||||||
|
| Offset from the Stack Pointer | Holds |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 | Status |
|
||||||
|
| 2 | Q |
|
||||||
|
| 3 | A |
|
||||||
|
| 4 | B |
|
||||||
|
| 5 and 6 | Data Pointer 3, high byte then low |
|
||||||
|
| 7 and 8 | Data Pointer 2, high byte then low |
|
||||||
|
| 9 and 10 | Data Pointer 1, high byte then low |
|
||||||
|
| 11 and 12 | Data Pointer 0, high byte then low |
|
||||||
|
| 13 and 14 | The address to resume at, high byte then low |
|
||||||
|
|
||||||
|
Writing to those bytes changes what RETI restores. Adding one to the address at offsets 13 and 14 is how a fault handler steps over the byte that failed and carries on, and rewriting the saved registers is how a handler hands something back to the code it interrupted.
|
||||||
|
|
||||||
|
SWI is never masked, because it is an instruction the program deliberately ran rather than something a device asked for.
|
||||||
|
|
||||||
|
## Hardware Interrupts:
|
||||||
|
|
||||||
|
A device asks for attention by putting its line up. Which line it uses is not a choice: a device on port N interrupts on N, and arrives through hardware vector N. That is what spares the machine any arbitration, and it means a program can work out what a device will do by knowing where it is plugged in.
|
||||||
|
|
||||||
|
A line is answered between instructions and never inside one, so the address in the frame is always the start of an instruction.
|
||||||
|
|
||||||
|
The Interrupt Flag decides whether lines are answered at all. While it is clear, a line that goes up stays up: masking holds a device off, it does not lose what the device was asking for. The moment the flag is set, the line is answered on the very next step. Since entry clears the flag again, a handler is not interrupted while it works unless it sets the flag itself.
|
||||||
|
|
||||||
|
When several lines are up at once, the lowest numbered port is answered first. This is a scan rather than a priority scheme, so there is nothing to configure and nothing to explain: a programmer works out what happens next by reading the port numbers.
|
||||||
|
|
||||||
|
Answering a line takes it down, so a device that wants attention again has to ask again. A handler returning with RETI restores the Status register, and with it the Interrupt Flag as it was before, so anything still waiting is answered next.
|
||||||
|
|
||||||
|
If a device interrupts and its vector is empty, that is a fault: the machine stops and the emulator says which port asked and where it was.
|
||||||
|
|
||||||
|
## Devices:
|
||||||
|
|
||||||
|
| Port | Device |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x00 | The console. Writing sends a byte to standard output, reading takes one from standard input. |
|
||||||
|
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. |
|
||||||
|
|
||||||
|
## Faults:
|
||||||
|
|
||||||
|
If the CPU reads a byte from Program Memory that does not decode to an instruction, it dispatches through Software Vector 2.
|
||||||
|
|
||||||
|
Faults get a vector each rather than sharing one. Vector 2 is the only cause defined so far, and vectors 3 through 15 are held back for the ones that come later, so that a handler always knows what happened from the entry it arrived through. That is why the machine has no fault cause register to read.
|
||||||
|
|
||||||
|
The address in the frame is the address of the offending byte itself, not the one after it. A handler can therefore read the byte that failed and say what it was. It also means a handler that returns with a bare RETI will meet the same byte again, because resuming past a fault means deciding where to resume, and only the handler knows that.
|
||||||
|
|
||||||
|
If nothing is installed at Vector 2, the CPU sets the Fault Flag and the Halt Flag and stops, leaving the Program Counter on the offending byte. The emulator then reports the byte and its address, and exits with a non zero status.
|
||||||
|
|
||||||
|
Stopping matters because the alternative is worse. A byte that means nothing is almost always a sign that execution has wandered into data, or that a program was built for a machine with instructions this one does not have. Stepping over it and carrying on turns a clear failure into a program that appears to run and quietly does the wrong thing.
|
||||||
|
|
||||||
## Naming a Data Pointer:
|
## Naming a Data Pointer:
|
||||||
|
|
||||||
Twelve instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
|
Sixteen instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
|
||||||
|
|
||||||
The selector is a full byte, but only enough of it is read to choose among the Data Pointers the machine has. A selector larger than the highest numbered pointer wraps around rather than being rejected, so it is the assembler's job to refuse to write one.
|
The selector is a full byte, but only enough of it is read to choose among the Data Pointers the machine has. A selector larger than the highest numbered pointer wraps around rather than being rejected, so it is the assembler's job to refuse to write one.
|
||||||
|
|
||||||
@@ -60,7 +164,7 @@ The Bytes column is the total length of the instruction, counting its opcode, an
|
|||||||
| 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. |
|
| 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. |
|
||||||
| 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. |
|
| 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. |
|
||||||
|
|
||||||
### Branch and Subroutine Operations: 7 Instructions
|
### Branch and Subroutine Operations: 8 Instructions
|
||||||
| Hex Code | Mnemonic | Bytes | Description |
|
| Hex Code | Mnemonic | Bytes | Description |
|
||||||
| -- | ---- | -- | -- |
|
| -- | ---- | -- | -- |
|
||||||
| 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
|
| 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
|
||||||
@@ -68,10 +172,13 @@ The Bytes column is the total length of the instruction, counting its opcode, an
|
|||||||
| 12 | BRA | 3 | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
| 12 | BRA | 3 | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||||
| 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
| 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||||
| 14 | BRC | 3 | Branch if Carry is set. |
|
| 14 | BRC | 3 | Branch if Carry is set. |
|
||||||
|
| 15 | BRD | 2 | Branch to the address held in the named Data Pointer. |
|
||||||
| 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. |
|
| 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. |
|
||||||
|
| 18 | SWI | 2 | Software Interrupt. The next byte names a software vector. Pushes an interrupt frame and dispatches through it. Never masked. |
|
||||||
|
| 19 | RETI | 1 | Return from an interrupt. Restores everything the frame holds and carries on from where the interrupt arrived. |
|
||||||
| 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. |
|
| 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. |
|
||||||
|
|
||||||
### Register Operations: 9 Instructions
|
### Register Operations: 11 Instructions
|
||||||
| Hex Code | Mnemonic | Bytes | Description |
|
| Hex Code | Mnemonic | Bytes | Description |
|
||||||
| -- | ---- | -- | -- |
|
| -- | ---- | -- | -- |
|
||||||
| 20 | RSTA | 1 | Resets A to 0. |
|
| 20 | RSTA | 1 | Resets A to 0. |
|
||||||
@@ -83,6 +190,12 @@ The Bytes column is the total length of the instruction, counting its opcode, an
|
|||||||
| 26 | INIA | 2 | Loads the next byte of Program Memory to A. |
|
| 26 | INIA | 2 | Loads the next byte of Program Memory to A. |
|
||||||
| 27 | INIB | 2 | Loads the next byte of Program Memory to B. |
|
| 27 | INIB | 2 | Loads the next byte of Program Memory to B. |
|
||||||
| 28 | CCF | 1 | Clears the Carry Flag. |
|
| 28 | CCF | 1 | Clears the Carry Flag. |
|
||||||
|
| 29 | MVQA | 1 | Copies Q into A. No flags are changed. |
|
||||||
|
| 2A | MVQB | 1 | Copies Q into B. No flags are changed. |
|
||||||
|
| 2B | SIF | 1 | Sets the Interrupt Flag. No other flags are changed. |
|
||||||
|
| 2C | CIF | 1 | Clears the Interrupt Flag. No other flags are changed. |
|
||||||
|
|
||||||
|
Q is where every ALU result lands, and Q is not itself an ALU operand, so MVQA and MVQB are how a result becomes the input to the next sum. Without them the only route is to store Q into Data Memory and load it back, which costs two instructions and needs a Data Pointer aimed somewhere useful. With them a running total can be kept in the registers and never touch memory at all.
|
||||||
|
|
||||||
### Stack Operations: 7 Instructions
|
### Stack Operations: 7 Instructions
|
||||||
| Hex Code | Mnemonic | Bytes | Description |
|
| Hex Code | Mnemonic | Bytes | Description |
|
||||||
@@ -110,6 +223,9 @@ The Bytes column is the total length of the instruction, counting its opcode, an
|
|||||||
| 49 | DPDN | 3 | Offsets the named Data Pointer down by the value of the byte following the selector. |
|
| 49 | DPDN | 3 | Offsets the named Data Pointer down by the value of the byte following the selector. |
|
||||||
| 4A | LDD | 3 | Loads the first named Data Pointer from the two bytes of Data Memory addressed by the second, most significant byte first. |
|
| 4A | LDD | 3 | Loads the first named Data Pointer from the two bytes of Data Memory addressed by the second, most significant byte first. |
|
||||||
| 4B | STD | 3 | Stores the first named Data Pointer into the two bytes of Data Memory addressed by the second, most significant byte first. |
|
| 4B | STD | 3 | Stores the first named Data Pointer into the two bytes of Data Memory addressed by the second, most significant byte first. |
|
||||||
|
| 4C | MVSD | 2 | Copies the Stack Pointer into the named Data Pointer. The Stack Pointer itself is unchanged and still cannot be written. |
|
||||||
|
|
||||||
|
BRD is the only branch whose destination is not written into the program. Every other branch carries the address it goes to, fixed when the program was assembled; BRD takes it from a Data Pointer, which is what makes a table of addresses something a program can dispatch through rather than only read. Together with LDD it turns the Data Segment into somewhere a program can keep a list of places to go.
|
||||||
|
|
||||||
LDD and STD are how a program follows an address it has stored, rather than one the assembler wrote into the instruction. Together with more than one Data Pointer, they are what makes a table of addresses usable: one pointer walks the table while another follows whatever entry it is on. Naming the same pointer twice, as in `LDD.0.0`, makes that pointer follow the address it is currently holding.
|
LDD and STD are how a program follows an address it has stored, rather than one the assembler wrote into the instruction. Together with more than one Data Pointer, they are what makes a table of addresses usable: one pointer walks the table while another follows whatever entry it is on. Naming the same pointer twice, as in `LDD.0.0`, makes that pointer follow the address it is currently holding.
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
0000 0001 0001 0002 0003 0005 0008 000D 0015 0022 0037 0059 0090 00E9 0179 0262 03DB 063D 0A18 1055 1A6D 2AC2 452F 6FF1
|
0000 0001 0001 0002 0003 0005 0008 000D 0015 0022 0037 0059 0090 00E9 0179 0262 03DB 063D 0A18 1055 1A6D 2AC2 452F 6FF1
|
||||||
Execution halted after 2534 cycles.
|
Execution halted after 2534 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -223,3 +223,4 @@
|
|||||||
|
|
||||||
|
|
||||||
Execution stopped after 3000000 cycles. (cycle limit reached)
|
Execution stopped after 3000000 cycles. (cycle limit reached)
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
[2J[H #
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
#
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
#
|
||||||
|
##
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
#
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
#
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
#
|
||||||
|
##
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
##
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# #
|
||||||
|
##
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Execution stopped after 3000000 cycles. (cycle limit reached)
|
||||||
|
[exit 0]
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
00000000 00000001 00000001 00000002 00000003 00000005 00000008 0000000D 00000015 00000022 00000037 00000059 00000090 000000E9 00000179 00000262 000003DB 0000063D 00000A18 00001055 00001A6D 00002AC2 0000452F 00006FF1 0000B520 00012511 0001DA31 0002FF42 0004D973 0007D8B5 000CB228 00148ADD 00213D05 0035C7E2 005704E7 008CCCC9 00E3D1B0 01709E79 02547029 03C50EA2 06197ECB 09DE8D6D 0FF80C38 19D699A5 29CEA5DD 43A53F82 6D73E55F
|
00000000 00000001 00000001 00000002 00000003 00000005 00000008 0000000D 00000015 00000022 00000037 00000059 00000090 000000E9 00000179 00000262 000003DB 0000063D 00000A18 00001055 00001A6D 00002AC2 0000452F 00006FF1 0000B520 00012511 0001DA31 0002FF42 0004D973 0007D8B5 000CB228 00148ADD 00213D05 0035C7E2 005704E7 008CCCC9 00E3D1B0 01709E79 02547029 03C50EA2 06197ECB 09DE8D6D 0FF80C38 19D699A5 29CEA5DD 43A53F82 6D73E55F
|
||||||
Execution halted after 10610 cycles.
|
Execution halted after 10610 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
0 1 1 2 3 5 8 13 21 34 55 89 144 233
|
0 1 1 2 3 5 8 13 21 34 55 89 144 233
|
||||||
Execution halted after 1506 cycles.
|
Execution halted after 1506 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
|
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
|
||||||
Execution halted after 54061 cycles.
|
Execution halted after 54061 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
ABCZ
|
ABCZ
|
||||||
Execution halted after 21 cycles.
|
Execution halted after 21 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
O
|
||||||
|
K
|
||||||
|
!
|
||||||
|
Execution halted after 19 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
done
|
||||||
|
Execution halted after 126 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
O
|
||||||
|
K
|
||||||
|
Execution halted after 17 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Fault: 0xFE at Program Address 0x0002 is not an instruction.
|
||||||
|
Execution halted after 2 cycles.
|
||||||
|
[exit 1]
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
Hello, World!
|
Hello, World!
|
||||||
Execution halted after 70 cycles.
|
Execution halted after 70 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Input Test: Will echo anything you put in.
|
Input Test: Will echo anything you put in.
|
||||||
Hello SplitBit
|
Hello SplitBit
|
||||||
Execution halted after 406 cycles.
|
Execution halted after 406 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Input Test: Will echo anything you put in.
|
Input Test: Will echo anything you put in.
|
||||||
Hello SplitBit
|
Hello SplitBit
|
||||||
Execution halted after 406 cycles.
|
Execution halted after 406 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
00 00 00 00 01
|
00 00 00 00 01
|
||||||
1
|
1
|
||||||
Execution halted after 4707 cycles.
|
Execution halted after 4707 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ready
|
||||||
|
trap
|
||||||
|
device
|
||||||
|
Execution halted after 105 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
OKC
|
||||||
|
Execution halted after 19 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Fault: The device on port 16 interrupted at Program Address 0x0019, and hardware vector 16 has no handler installed.
|
||||||
|
M
|
||||||
|
S
|
||||||
|
Execution halted after 16 cycles.
|
||||||
|
[exit 1]
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
0000 0032
|
0000 0032
|
||||||
Execution halted after 1134 cycles.
|
Execution halted after 1134 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
AAA
|
||||||
|
Execution halted after 24 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
00 31
|
||||||
|
Execution halted after 78 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -2,3 +2,4 @@ Hello
|
|||||||
World
|
World
|
||||||
H
|
H
|
||||||
Execution halted after 79 cycles.
|
Execution halted after 79 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Hello, World!
|
Hello, World!
|
||||||
42 is the great answer.
|
42 is the great answer.
|
||||||
Execution halted after 295 cycles.
|
Execution halted after 295 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ Testing printByteHex...
|
|||||||
|
|
||||||
Testing complete!
|
Testing complete!
|
||||||
Execution halted after 71185 cycles.
|
Execution halted after 71185 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ SplitBit calculator (+ - * & | ^), Q quits.
|
|||||||
> 18
|
> 18
|
||||||
|
|
||||||
> Goodbye!Execution halted after 2960 cycles.
|
> Goodbye!Execution halted after 2960 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
OK
|
||||||
|
Execution halted after 14 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -3,3 +3,4 @@ two
|
|||||||
three
|
three
|
||||||
AFTER
|
AFTER
|
||||||
Execution halted after 122 cycles.
|
Execution halted after 122 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Fault: Software vector 20, dispatched from Program Address 0x0008, has no handler installed.
|
||||||
|
O
|
||||||
|
Execution halted after 5 cycles.
|
||||||
|
[exit 1]
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
Two pointers, no stack shenanigans.
|
Two pointers, no stack shenanigans.
|
||||||
Execution halted after 395 cycles.
|
Execution halted after 395 cycles.
|
||||||
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
OK!
|
||||||
|
good
|
||||||
|
AFTER
|
||||||
|
Execution halted after 77 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -29,6 +29,9 @@ printHello | printHello.asm | run | -
|
|||||||
32bitFibonacci | Fibonacci/32bitFibonacci.asm | run | - | -
|
32bitFibonacci | Fibonacci/32bitFibonacci.asm | run | - | -
|
||||||
8bitSieve | primeSieve/8bitSieve.asm | run | - | -
|
8bitSieve | primeSieve/8bitSieve.asm | run | - | -
|
||||||
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | run | - | -
|
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | run | - | -
|
||||||
|
# The four pointer rewrite. It emits exactly the same primes as the line above, which
|
||||||
|
# is the whole point of keeping both: the pair is a direct before and after.
|
||||||
|
16bitSegmentedSieveModern | primeSieve/16bitSegmentedSieveModern.asm | run | - | -
|
||||||
mathTest | testPrograms/mathTest.asm | run | - | -
|
mathTest | testPrograms/mathTest.asm | run | - | -
|
||||||
printTest | testPrograms/printTest.asm | run | - | -
|
printTest | testPrograms/printTest.asm | run | - | -
|
||||||
int16print | Libraries/int16print.asm | run | - | -
|
int16print | Libraries/int16print.asm | run | - | -
|
||||||
@@ -38,6 +41,47 @@ dataPointerTest | testPrograms/dataPointerTest.asm | run | -
|
|||||||
twoPointerCopy | testPrograms/twoPointerCopy.asm | run | - | -
|
twoPointerCopy | testPrograms/twoPointerCopy.asm | run | - | -
|
||||||
pointerTableTest | testPrograms/pointerTableTest.asm | run | - | -
|
pointerTableTest | testPrograms/pointerTableTest.asm | run | - | -
|
||||||
staticTableTest | testPrograms/staticTableTest.asm | run | - | -
|
staticTableTest | testPrograms/staticTableTest.asm | run | - | -
|
||||||
|
dispatchTest | testPrograms/dispatchTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Moving an ALU result back into an operand register ----
|
||||||
|
moveQTest | testPrograms/moveQTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Moving the cursor along ----
|
||||||
|
paddingTest | testPrograms/paddingTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Finding the Stack ----
|
||||||
|
stackPointerTest | testPrograms/stackPointerTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- The Interrupt Flag ----
|
||||||
|
# Nothing reads the flag yet. This checks that setting and clearing it leaves
|
||||||
|
# the registers and the Carry Flag it shares a byte with untouched.
|
||||||
|
interruptFlagTest | testPrograms/interruptFlagTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Vectors laid down by the assembler ----
|
||||||
|
# These are the interrupt path end to end. Until #Vectors existed, none of them could
|
||||||
|
# be written as a source file at all, because nothing could install a handler.
|
||||||
|
vectorTest | testPrograms/vectorTest.asm | run | - | -
|
||||||
|
deviceTest | testPrograms/deviceTest.asm | run | - | -
|
||||||
|
faultResumeTest | testPrograms/faultResumeTest.asm | run | - | -
|
||||||
|
# The worked example out of the Assembler Manual, so the manual cannot go stale.
|
||||||
|
interruptExample | testPrograms/interruptExample.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Holding a device off, and then letting it through ----
|
||||||
|
# Faults on purpose. The fault is the proof that the line was answered, and where it
|
||||||
|
# lands in the output is the proof of when.
|
||||||
|
maskTest | testPrograms/maskTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Dispatching through a vector with nothing in it ----
|
||||||
|
# Also meant to fault. Until the assembler can lay a vector table down, every entry
|
||||||
|
# reads as zero, so this is the only half of the interrupt path a source file can
|
||||||
|
# reach on its own. The round trip through a handler is covered by hand built
|
||||||
|
# binaries until #Vectors arrives.
|
||||||
|
swiFaultTest | testPrograms/swiFaultTest.asm | run | - | -
|
||||||
|
|
||||||
|
# ---- Meeting a byte that is not an instruction ----
|
||||||
|
# This one is meant to fault. It checks the CPU stops, says what it found and
|
||||||
|
# where, and exits non zero, rather than stepping over it and carrying on.
|
||||||
|
faultTest | testPrograms/faultTest.asm | run | - | -
|
||||||
|
|
||||||
# ---- Programs driven by console input ----
|
# ---- Programs driven by console input ----
|
||||||
inputTest | inputTest.asm | run | inputTest.in | -
|
inputTest | inputTest.asm | run | inputTest.in | -
|
||||||
@@ -48,6 +92,12 @@ replCalculator | replCalculator.asm | run | replCalcu
|
|||||||
# 3,000,000 cycles is about fourteen generations of the glider, which puts
|
# 3,000,000 cycles is about fourteen generations of the glider, which puts
|
||||||
# evolveBoard and its pointer juggling through its paces many times over.
|
# evolveBoard and its pointer juggling through its paces many times over.
|
||||||
16x16Life | gameOfLife/16x16Life.asm | run | - | 3000000
|
16x16Life | gameOfLife/16x16Life.asm | run | - | 3000000
|
||||||
|
# The four pointer rewrite, on the same budget so the two can be compared directly.
|
||||||
|
# Note that this cannot show a speed difference: frameDelay is 255 by 255 and swamps
|
||||||
|
# the simulation, so both versions render the same fourteen generations and produce
|
||||||
|
# identical bytes. What it checks is that the rewrite still evolves the board the same
|
||||||
|
# way, which is what a regression test is for.
|
||||||
|
16x16LifeModern | gameOfLife/16x16LifeModern.asm | run | - | 3000000
|
||||||
|
|
||||||
# ---- Libraries: no entry point, so only check that they assemble ----
|
# ---- Libraries: no entry point, so only check that they assemble ----
|
||||||
lib-int8 | Libraries/int8.asm | assemble | - | -
|
lib-int8 | Libraries/int8.asm | assemble | - | -
|
||||||
@@ -55,6 +105,18 @@ lib-int16 | Libraries/int16.asm | assemble | -
|
|||||||
lib-int32 | Libraries/int32.asm | assemble | - | -
|
lib-int32 | Libraries/int32.asm | assemble | - | -
|
||||||
lib-math | Libraries/math.asm | assemble | - | -
|
lib-math | Libraries/math.asm | assemble | - | -
|
||||||
|
|
||||||
|
# ---- Deliberately broken, to check the assembler still diagnoses them ----
|
||||||
|
# These are not programs anyone meant to run. Each one contains a single mistake
|
||||||
|
# that the assembler used to accept quietly, and is here so that it cannot start
|
||||||
|
# being accepted quietly again.
|
||||||
|
diagDuplicateLabel | testPrograms/diagnostics/duplicateLabel.asm | xfail | - | -
|
||||||
|
diagBareInclude | testPrograms/diagnostics/bareInclude.asm | xfail | - | -
|
||||||
|
diagUnknownVector | testPrograms/diagnostics/unknownVector.asm | xfail | - | -
|
||||||
|
diagDuplicateVector | testPrograms/diagnostics/duplicateVector.asm | xfail | - | -
|
||||||
|
diagBareSWI | testPrograms/diagnostics/bareSWI.asm | xfail | - | -
|
||||||
|
diagAlignOutside | testPrograms/diagnostics/alignOutside.asm | xfail | - | -
|
||||||
|
diagBareAlign | testPrograms/diagnostics/bareAlign.asm | xfail | - | -
|
||||||
|
|
||||||
# ---- Known breakages, recorded rather than ignored ----
|
# ---- Known breakages, recorded rather than ignored ----
|
||||||
# print.asm branches to 'start', which only the including program defines.
|
# print.asm branches to 'start', which only the including program defines.
|
||||||
lib-print | Libraries/print.asm | xfail | - | -
|
lib-print | Libraries/print.asm | xfail | - | -
|
||||||
|
|||||||
+8
-1
@@ -166,11 +166,18 @@ while IFS='|' read -r name src mode stdin limit; do
|
|||||||
# bounds them by cycle count rather than by wall clock.
|
# bounds them by cycle count rather than by wall clock.
|
||||||
EMUARGS=(--fast)
|
EMUARGS=(--fast)
|
||||||
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
|
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
|
||||||
if ! timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1; then
|
timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1
|
||||||
|
STATUS=$?
|
||||||
|
if [ "$STATUS" -eq 124 ]; then
|
||||||
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||||
report "FAIL" "$name" "did not finish within ${RUN_TIMEOUT}s"
|
report "FAIL" "$name" "did not finish within ${RUN_TIMEOUT}s"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
# What a program exits with is part of what it does, so it is recorded
|
||||||
|
# with the output rather than thrown away. A program that faults is
|
||||||
|
# supposed to exit non zero, and that should be just as pinned down as
|
||||||
|
# what it printed.
|
||||||
|
printf '[exit %d]\n' "$STATUS" >> "$OUT"
|
||||||
check "$name" "$OUT"
|
check "$name" "$OUT"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ PREFIX ?= /usr/local
|
|||||||
# editing a header rebuilds everything that includes it.
|
# editing a header rebuilds everything that includes it.
|
||||||
DEPFLAGS = -MMD -MP
|
DEPFLAGS = -MMD -MP
|
||||||
|
|
||||||
|
# Both tools use POSIX interfaces that ISO C does not have: realpath, clock_gettime,
|
||||||
|
# strdup, dirname and getopt. Asking for POSIX.1-2008 by name means the build does
|
||||||
|
# not rely on the compiler happening to default to a mode where those are visible,
|
||||||
|
# and it survives someone overriding CFLAGS, which is why it is kept separate.
|
||||||
|
POSIXFLAGS = -D_POSIX_C_SOURCE=200809L
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
SRC_DIR_EMU = Source/Emulator
|
SRC_DIR_EMU = Source/Emulator
|
||||||
SRC_DIR_ASM = Source/Assembler
|
SRC_DIR_ASM = Source/Assembler
|
||||||
@@ -41,12 +47,12 @@ $(ASM_TARGET): $(ASM_OBJS)
|
|||||||
# Compile emulator source files to object files
|
# Compile emulator source files to object files
|
||||||
$(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
|
$(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
|
||||||
mkdir -p $(OBJ_DIR)
|
mkdir -p $(OBJ_DIR)
|
||||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
# Compile assembler source files to object files
|
# Compile assembler source files to object files
|
||||||
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
||||||
mkdir -p $(OBJ_DIR)
|
mkdir -p $(OBJ_DIR)
|
||||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
# Pull in the header dependencies written out by the compiler above.
|
# Pull in the header dependencies written out by the compiler above.
|
||||||
-include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d)
|
-include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d)
|
||||||
@@ -55,6 +61,28 @@ $(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
|||||||
test: $(EMU_TARGET) $(ASM_TARGET)
|
test: $(EMU_TARGET) $(ASM_TARGET)
|
||||||
@./Tests/run.sh
|
@./Tests/run.sh
|
||||||
|
|
||||||
|
# Rebuild both tools with the address and undefined behaviour sanitizers and run
|
||||||
|
# the test suite under them. Slower than 'make test', and worth running before a
|
||||||
|
# release or after anything that touches memory handling.
|
||||||
|
#
|
||||||
|
# The sanitizers catch reads and writes off the end of an array, use after free,
|
||||||
|
# leaks, and undefined arithmetic. They also fill fresh allocations with a junk
|
||||||
|
# pattern, which is what turns a read of uninitialised memory from something that
|
||||||
|
# quietly works into something the tests notice.
|
||||||
|
#
|
||||||
|
# If the suite fails, the sanitizer binaries are deliberately left in place so
|
||||||
|
# that the failing case can be run again by hand. 'make' puts the normal ones back.
|
||||||
|
SANITIZE_FLAGS = -Wall -Wextra -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer
|
||||||
|
|
||||||
|
sanitize:
|
||||||
|
@$(MAKE) --no-print-directory clean
|
||||||
|
@$(MAKE) --no-print-directory CFLAGS="$(SANITIZE_FLAGS)"
|
||||||
|
@echo "Running the test suite under AddressSanitizer and UndefinedBehaviorSanitizer."
|
||||||
|
@./Tests/run.sh
|
||||||
|
@$(MAKE) --no-print-directory clean
|
||||||
|
@$(MAKE) --no-print-directory
|
||||||
|
@echo "Sanitizer run finished cleanly. Normal binaries rebuilt."
|
||||||
|
|
||||||
# Record the current output of every test as the expected result.
|
# Record the current output of every test as the expected result.
|
||||||
# Only do this when the current output is known to be correct.
|
# Only do this when the current output is known to be correct.
|
||||||
bless: $(EMU_TARGET) $(ASM_TARGET)
|
bless: $(EMU_TARGET) $(ASM_TARGET)
|
||||||
@@ -72,4 +100,4 @@ install: $(EMU_TARGET) $(ASM_TARGET)
|
|||||||
install -m 755 $^ "$(PREFIX)/bin/"
|
install -m 755 $^ "$(PREFIX)/bin/"
|
||||||
|
|
||||||
# Phony targets
|
# Phony targets
|
||||||
.PHONY: all clean install test bless
|
.PHONY: all clean install test bless sanitize
|
||||||
|
|||||||
Reference in New Issue
Block a user