Category: Veronica

Veronica – ROM Monitor

Booting into a useful state.

 

A common feature of 1980s computers is that they booted into a useful state. This was one of the main things that separated them from the hobbyist and trainer boards that preceded them. You could plunk an Apple ][ or a TRS-80 on your desk, flip the switch, and it would boot up into a state that allowed you to make it do something. This sounds ridiculously obvious now, but it was a big deal at the time. It was when home computers crossed the threshold from toy to tool.

Veronica needs to cross that threshold now. Her creator is already kind of a tool, but so far Veronica is still a toy. What we need is a ROM monitor. A very basic piece of software that will allow you to do a little something on the machine besides stare at a blinking light. In my case, the significance is even greater. I’ve said from the beginning that I wanted to build a computer from scratch, and that my definition of “computer” was “a machine that you can use to write code for itself, on itself”. At the moment, Veronica is more like an embedded device or a game console. She can only be programmed by tethering another computer to her. Let’s see what we can do about that.

Before we dive into this, I should mention my assembly macros. I’m developing with a ca65-based pipeline, and one of the awesome features is a powerful macro language. That goes a very long way to making assembly easy to write and understand. Listed below is my typical set of macros. They should all be self-explanatory except for a couple.

The first slightly weird one is CALL16. That’s a shortcut for calling functions with a 16-bit argument. I have some standard 6502 zero-page areas specified, and one of those holds arguments for function calls. Implementing a high-level generalized stack-based function calling convention on the 6502 is a pretty complex task, and not something I need for this little bit of ROM code. Instead, I simply pass function parameters in pre-designated places on the zero-page. The code is faster, smaller, and simpler to read this way. It’s more limited, because you don’t have a proper local context for each function call. You need to be aware that you’re stepping on the previous context when calling a new function, so nesting calls must be done carefully. We’re programming without a net at this low level.

The other slightly odd macros are the GPU commands (static and variable versions). These are convenience routines for passing the two-byte commands through the memory-mapped GPU command register at memory location $EFFF. This is a command byte followed by a parameter byte. Each two-byte command is queued up and processed on the GPU when it has time. This is all documented on the various GPU pages of Veronica’s blog entries.

.macro GPUCMD	cmdByte,paramByte
	lda		#cmdByte
	sta		$efff
	lda		#paramByte
	sta		$efff
.endmacro


.macro GPUVCMD	cmdByte,paramByte
	lda		#cmdByte
	sta		$efff
	lda		paramByte
	sta		$efff
.endmacro


;
; Function calling
;
.macro CALL16	subroutine,param16
	lda		#<param16
	sta		PARAM1_L
	lda		#>param16
	sta		PARAM1_H
	jsr		subroutine
.endmacro


;
; Stack management
;
.macro SAVE_AXY
	pha
	txa
	pha
	tya
	pha
.endmacro

.macro RESTORE_AXY
	pla
	tay
	pla
	tax
	pla
.endmacro

.macro SAVE_AY
	pha
	tya
	pha
.endmacro

.macro RESTORE_AY
	pla
	tay
	pla
.endmacro

.macro SAVE_AX
	pha
	txa
	pha
.endmacro

.macro RESTORE_AX
	pla
	tax
	pla
.endmacro

 

Okay, with the housekeeping out of the way, let’s get down to business. I already have some input routines that I wrote for my keyboard interface, so we’ll be leveraging that. After checking all the RAM, Veronica will boot up into a loop that takes a command line of input:

;;;;;;;;;;;;;;;;;;;;;;;
; monStart
; Args: None
;
monStart:
	SAVE_AXY

	GPUCMD	CLEARSCR,$00
	GPUCMD	CURSORXPOS,0
	GPUCMD	CURSORYPOS,4

	CALL16	printStr,monIntro

monLoop:
	jsr		monClearInput

	GPUCMD	CURSORXPOS,0
	GPUCMD	CURSORYPOS,5
	CALL16	printStr,monPrompt

	jsr		monReadLine
	jsr		monProcessCmd
	jmp		monLoop


	RESTORE_AXY		; For completeness. No way to actually exit the monitor anyway
	rts


;;;;;;;;;;;;;;;;;;;;;;;
; monReadLine
; Args: None
; Out:	monCommandBuf:	The command string read
;
monReadLine:
	SAVE_AXY

	ldy		#0

monReadLineLoop:
	jsr		processKeyboard		; Wait for a line of input
	jsr		getCh
	ldx		RETURN_L
	beq		monReadLineLoop

	cpx		#13
	beq		monReadLineDone

	txa
	sta		monCommandBuf,Y
	iny
	cpy		#38
	beq		monReadLineDone		; Hit the end of the input buffer

	GPUVCMD	PLOTSTR,RETURN_L	; Render characters as typed
	jmp		monReadLineLoop

monReadLineDone:
	lda		#0					; Null-terminate the string
	sta		monCommandBuf,Y

	RESTORE_AXY
	rts

Once we have the command line as a null-terminated string in a buffer, we parse the command and match it up to known commands. This is done using a jump-table, which is a very powerful assembly technique to which the 6502 is well-suited.

;;;;;;;;;;;;;;;;;;;;;;;
; monProcessCmd
; Args:	monCommandBuf:	The command string to process
;
monProcessCmd:
	SAVE_AXY

	ldy		#0

monProcessCmdLookup:		; Figure out which command was entered
	lda		monCommands,Y
	beq		monProcessCmdInvalid

	lda		monCommandBuf
	cmp		monCommands,Y
	beq		monProcessCmdMatch

	iny
	iny
	iny
	jmp		monProcessCmdLookup

monProcessCmdMatch:
	iny
	lda		monCommands,Y		; Prepare to bounce off the jump table
	sta		monCommandBounce_L
	iny
	lda		monCommands,Y
	sta		monCommandBounce_H
	jmp		(monCommandBounce_L)	; Rely on jump target to rts


monProcessCmdInvalid:

	RESTORE_AXY
	rts
;;;;;;;;;;;;;;;;;;;;;;;
; Command table
;
monCommands:
.byte		"R"
.addr		monCmdRead
.byte		"W"
.addr		monCmdWrite
.byte		"C"
.addr		monCmdClear
.byte		"G"
.addr		monCmdGo
.byte		"P"
.addr		monCmdPong

.byte		0					; Command table terminator

Now to write the commands themselves. The main job of a ROM monitor is to examine, modify, and execute memory. These three things are the minimum requirement to program a computer. My “Read” command takes a starting address and a number of bytes, then displays that memory onscreen in a  human-readable format.

;;;;;;;;;;;;;;;;;;;;;;;
; Monitor Command jump table targets
;

;;;;;;;;;;;;;;;;;;;;;;;

monCmdRead:

	clc							; Read starting address
	lda		#MON_CMD_BUF_L
	adc		#2
	sta		PARAM1_L
	lda		#MON_CMD_BUF_H
	sta		PARAM1_H

	jsr		scanHex16
	lda		RETURN_L
	sta		SCRATCHA_L
	lda		RETURN_H
	sta		SCRATCHA_H

	ldy		#7					; Read byte count
	lda		monCommandBuf,Y
	sta		PARAM1_L

	iny
	lda		monCommandBuf,Y
	sta		PARAM1_H

	jsr		scanHex8
	ldx		RETURN_L

monCmdDisplayMemory:		; Byte count should be in X
	ldy		#0
	GPUCMD	PLOTSTR,13

monCmdReadLoopOuter:

	txa
	pha
	ldx		#8

	GPUCMD	PLOTSTR,13
	jsr		delayShort		; Let the GPU catch up

	tya						; Show address
	clc
	adc		SCRATCHA_L
	sta		PARAM1_L
	lda		SCRATCHA_H
	adc		#0
	sta		PARAM1_H
	jsr		printHex16
	GPUCMD	PLOTSTR,':'
	GPUCMD	PLOTSTR,' '

monCmdReadLoopInner:		; Show row of 8 bytes

	lda		(SCRATCHA_L),Y
	sta		PARAM1_L
	jsr		printHex8

	GPUCMD	PLOTSTR,' '

	iny
	dex
	bne		monCmdReadLoopInner

	pla
	tax
	dex
	bne		monCmdReadLoopOuter

monCmdReadDone:
	RESTORE_AXY
	rts

Here’s a video of booting up into ROM, and using the Read command to look at some memory. At startup, RAM tends to be filled with $42, because that’s a special value used by the RAM diagnostic.

Now we need to be able to modify memory. That’s done with the Write command. I won’t go over all the code for all the commands here, because they’re quite similar. The Write command takes a starting address, then an arbitrary list of space-delimited hex bytes. Here’s a demonstration of  examining memory at $4000, then writing some NOPs into that same area. After each modification, the Write command automatically displays the memory you modified so you can see the change.

The command to execute code (“Go”) is the last piece of the puzzle, and it’s trivial. Here’s a demonstration that brings it all together. I fire up Veronica’s ROM monitor, enter some code into memory (hand assembled) to display an ASCII character set, then run it. With this act, I have proven that Veronica meets my definition of “computer”. You can use her to write code for herself, entirely untethered to any other devices.

Oh, just more thing. Since this ROM monitor isn’t very exciting with just those housekeeping commands, and I have those gamepads ready to go, I added this:

Every ROM monitor should have a “Pong” command, if you ask me.

Look mom- I made a computer!

Hacks Veronica