A proposed DMA-like I/O system for DCPU-16

Contents

  1. I/O System Specification
  2. Devices
  3. FAQ
  4. Change History

Back to Top

I/O System Specification

This document describes a simple, easy to use, yet very powerful, I/O System for the DCPU-16 processor used in the 0x10c game, as currently specified by Notch in the DCPU-16 Specification, version 1.1. It was inspired by discussions on the 0x10c Forum and the DCPU-16 Programming Reddit. An earlier draft of this design was presented on both sites and the general concept received much positive feedback. I've updated and corrected the design in creating this webpage based on a lot of very good feedback I received from both sites. A thank you to all those who responded and helped make this a better idea.

Back to Top

Benefits

The system described here provides a number of useful benefits to the project:

Back to Top

Changes to the DCPU-16 1.1 spec

Before getting into the specifics of the I/O system, there are some changes to the current 1.1 spec which must be addressed:

Back to Top

The I/O System

The I/O System we're defining can be summarized in exactly two elements:

  1. A pair of opcodes to Read and Write a single word to an I/O location
  2. The ability of any directly-connected peripheral device to access the DCPU-16 memory space

That's the whole thing. Very simple, and very powerful, and the specific behavior of a device is now isolated from the processor specification. Before we get into the details, let me provide an example of how, say, a disk transfer might work. Given that you want to Read Sector 10 from a floppy, and store it at location 0x2000:

The I/O Opcodes

We define two new dual operand opcodes for reading and writing to I/O locations:

	IOR target, location		; Reads a word from the I/O location to the given target register/memory
	IOW location, source		; Writes the word from the source register/memory to the I/O location

Both instructions should take 2-3 cycles to execute, plus the cycles to evaluate operands. An IOR/IOW to a location which does not exist should have no effect. (Technically, therefore, one could probe I/O locations by doing IOR requests to them with a known value in the target register, and if the register does not change, the location is unconnected...)

If Notch doesn't reduce the A operand to 5 bits, there are alternate forms of these opcodes using the single operand opcode space (like JSR), described in the FAQ, here.

I/O Locations

I/O Locations are specified by a 16 bit I/O location address. (A "port address" for those who prefer to identify it that way, or a message destination address if you want to think of it that way. Either way, you're sending or receiving a single word from something outside of the CPU, defined by a 16 bit address.) I/O Locations for common devices are fixed. This specification does not make any assumptions other than that - how I/O locations are allocated for add-on peripherals is up to Notch.

There are several common types of behavior for peripherals to act when accessing an I/O location. We'll define four here, but the specification is not limited to these four behaviors:

  1. Control - A control location typically has a value which defines how a peripheral should behave, and takes no additional time to change that behavior. Writing and reading to control locations is symmetrical - values that you write to the location will be returned intact when you read the same location. An example of a Control location would be on the Video Display device, which could have a control location to specify the current video display mode (text/graphical). A UART device might have a Control Location to specify something like communications speed.
  2. Immediate - An immediate location does immediate, direct I/O when you read or write from it, that takes no additional time to perform. These are rarely symmetrical. A typical example of this would be the Keyboard device, where Reading from it would return the next keypress in the keyboard buffer, and writing might either do nothing, or perhaps set the state of the LEDs on the keyboard. A UART device might have an Immediate location to send a word on write, or receive the next word, if there is one, on read, and an Immediate location to set and get status and flow control bits.
  3. Action - An action location performs an action which might take a period of time when a value is written to it. When the location is read, it returns a status value indicating whether it's currently performing the action, whether it's complete, or whether the action produced an error of some sort. An example of an action location would be a Seek on the disk drive, where writing to it causes the Seek to begin, and reading from it tells you whether the seek is complete or not.
  4. DMA Transfer - A DMA transfer location, when written, triggers a DMA transfer between the peripheral. The value written is the memory address in the DCPU-16 memory space that will be the source or destination of the DMA transfer. As with an Action location, it returns the status of the transfer while it's in progress -- DMA transfers typically take a minimum of 1 cycle per memory location read or written, and might take longer depending on the device. (Notch could be really nice to us and make it less, but this is a more realistic timing scheme... :D ) Examples of DMA Transfer Locations include:

Back to Top

Devices

Everything else in the I/O system is defined by the peripheral devices themselves. In this section I'll be suggesting a number of possible peripheral devices, but they're all just suggestions, and in a few cases I may present alternative suggestions. The I/O System itself is flexible enough to handle an almost infinite variety of mechanisms for defining a peripheral device, up to and including intrusive devices which take over an area of memory for it's use without being asked to do so by an IOR/IOW instruction. (I would much prefer that no such devices exist, mind you, but if Notch wants to put in an unknown Alien device which, if you plug it into your computer, starts writing a seemingly garbage-like string to a location in memory, I'm not going to argue. It the player's own fault that he plugged the thing into his computer in the first place... :P )

A variety of suggestions have been made for device types -- I'm going to focus primarily on the core devices which are needed for basic computing purposes.

By convention, I'm putting all the standard, fixed devices in the I/O location range of 0x0000 - 0x00ff, but all the location addresses are subject to change.

Back to Top

Keyboard Device

I'm proposing a very simple keyboard device which has a small (8 - 32 word) buffer on board. If the buffer is full, it simply ignores further key presses. It has the following I/O locations:

I/O LocationNameBehavior ClassDescription
0x0000 Keyboard Immediate When this is read, it returns the next available entry in the keyboard buffer. The specifics of that depend on how Notch wants to implement keyboards: I could see it returning a word where the low octet was the character code, and the high octet any special modifier bits. Or I could see it returning a word where the low octet was a keycode, and the high octet was both modifier bits and a bit to indicate whether this is a key down or a key up. Writing to this register might set status bits for LEDs, but I'm defining only that if the low bit is 1, it empties the buffer.

Keyboard Example 1: Reading the next key from the keyboard

All you need to do to read a key is:

	IOR X, 0x0000			; Read the keyboard location and store it in X
	IFN X, 0			; If it's not zero...
		SET PC, gotkey		;    ...we got a key, so do something with it.

Keyboard Example 2: Reading keys into a buffer

A common task would be to read all the current keystrokes waiting for us, into a buffer. Here's an example subroutine:

read_keyboard:
	SET PUSH, X			; Save X - which is the address of the target buffer at entry
	SET A, 0			; The number of characters read
	
readchar:
	IOR [X], 0x0000			; Read the keyboard location and store it in X
	IFE [X], 0			; If it's zero...
		SET PC, endread		;    ...we're done, so finish up
	
	ADD A, 0x0001			; Increment A and X
	ADD X, 0x0001
	IFN A, Y			; If our buffer isn't full loop again
		SET PC, readchar
		
endread:
	SET X, POP			; Restore X and return.   A on return has the number of keystrokes read
	SET PC, POP

Back to Top

Video Device

I'm proposing a video device which has a character mode much like Notch has already shown in examples, a graphics mode of yet undetermined capabilities, a certain amount of "VRAM", a character glyph buffer which is used to drive the character generator in character mode, and a character glyph "ROM" with the ASCII character glyphs in it. By default, it starts with VRAM and it's control locations set to zero values, and at power up it copies the character glyph ROM contents to the character glyph buffer. Everything else is driven by the I/O locations:

I/O LocationNameBehavior ClassDescription
0x0008 Transfer Video Buffer DMA Transfer When an address is Written here, it will transfer N words from that address to the video buffer, where N is the number of words used in displaying the current video mode. Status on Read will be 0 if not doing a transfer, non-zero if a transfer is in progress. Specifying a new transfer while one is in progress will leave the screen in an indeterminate state and stop the original transfer in favor of the new transfer.
0x0009 Video Mode Control This control location is used to specify and read the current video mode. Currently the following modes are defined:
  • 0x0000 - Character Mode. 32 x 12 characters, with low octet of each word being the character to display and high octet being two 4-bit foreground and background color values. (Alternately, character octet could be 7 bits, with the high bit of that octet being a "blink" attribute - but I'd rather the 8 bit characters and let blinking be done away with...) The glyph displayed for a character comes from the character glyph buffer - which is initialized to standard ASCII glyphs.
  • 0x0001 - Color Graphics Mode. Specifics TBD.
0x000a Read Character Glyphs DMA Transfer When an address is Written here, the first word at that address defines a range of character glyphs, with the high octet specifying the glyph at the start of the range, and the low octet specifying the end. They may be equal to specify one glyph, but if the low octet is less than the high octet, no glyphs will be transferred. After reading the range, the video device will copy the 8x8 pixel bitmaps currently corresponding to those glyphs to memory immediately following the range word, at 4 words per glyph. Status on Read will be 0 if not doing a transfer, non-zero if a transfer is in progress. Specifying a new transfer while an existing transfer is in progress will stop the existing transfer, leaving that range of glyphs in an indeterminate state, and begin the new transfer.
0x000b Write Character Glyphs DMA Transfer When an address is Written here, the first word at that address defines a range of character glyphs, with the high octet specifying the glyph at the start of the range, and the low octet specifying the end. They may be equal to specify one glyph, but if the low octet is less than the high octet, no glyphs will be transferred. After reading the range, the video device will copy the 8x8 pixel bitmaps from memory immediately following the range word, into the character glyph buffer, at 4 words per glyph. Status on Read will be 0 if not doing a transfer, non-zero if a transfer is in progress. Specifying a new transfer while an existing transfer is in progress will stop the existing transfer, leaving that range of glyphs in an indeterminate state, and begin the new transfer.
0x000c Restore Default Character Glyphs Action When a word is written here, that word defines a range of character glyphs, with the high octet specifying the glyph at the start of the range, and the low octet specifying the end. They may be equal to specify one glyph, but if the low octet is less than the high octet, no glyphs will be restored. The video device will restore the specified glyphs to their original power up settings.

All of these registers except the Transfer Video Buffer register are added functionality - we could be operational with JUST that one register. But we'd obviously like to have the rest.

Video Example 1: Updating the screen from a local video buffer

Lets say you have defined your own text mode video buffer in DCPU-16 RAM at 0x6000. To copy that buffer to screen, all you need to do is:

	IOW 0x0008, 0x6000		; Write the location of your video buffer to the Transfer Video Buffer location - it'll do the rest.
In theory, in a 32 x 12 character mode, it will update your screen in about 387 cycles - which is incredibly fast compared to other double buffering techniques using memory mapped I/O. Of course, you still need to build, fill, and modify your own local video buffer, but it's ANYWHERE you want it to be in memory.

Video Example 2: Waiting for the video buffer transfer to be complete

Now, you might not want to modify your video buffer while the device is DMAing out your screen. So you should probably wait for the screen update to finish before you continue. Here's a function to do that:

wait_for_screen:
	SET PUSH, A			; Save A
wfsloop:
	IOR A, 0x0008			; Read the Transfer Video Buffer status
	IFN A, 0x00			; If it's doing something...
		SUB PC, 3		;    ...jump back to wfsloop

	SET A, POP			; Restore A
	SET PC, POP			; Return from subroutine

Video Example 3: Animate by cycling through an array of buffers

A nice thing about this is that you can build multiple virtual video buffers in your memory, then cycle through them at the speed of the DMA transfer. Lets say you've defined your buffers in allocated memory, and you have an array of pointers to those buffers ending in a 0 pointer. You enter your animate subroutine with the pointer to your "frames" array in X, and the number of times to animate through them in Y:

animate:
	SET PUSH, A			; Save A and Y
	SET PUSH, Y
	
animate_sequence:
	SET A, X			; Set A to the beginning of the array
	
frame_sequence:
	IFE [A],0			; If we're at the end of the array...
		SET PC, anim_checkend	;	Go see if we need to run through it again.
	JSR wait_for_screen		; Wait to see if the screen is done it's previous transfer
	IOW 0x0008, [A]			; Start a new transfer of the current frame
	SET PC, frame_sequence		; Loop for the next frame
	
anim_checkend:
	SUB Y, 1			; Decrement the animate loop counter
	IFN Y, 0			; If it's not yet zero, run the animate loop again
		SET PC, animate_sequence
	
	SET Y, POP			; Restore Y and A and return
	SET A, POP
	JMP PC, POP

Video Example 4: Scrolling through a buffer larger than the screen

Another nice thing about this is that you can define your video buffer to be bigger than the screen - say, 32 x 128, so that you can easily scroll it back down and see things which have scrolled off. Here's a function to scroll and redisplay the video buffer starting at X to start displaying at line Y, where the Y register should be a value from 0 - 116:

scroll_vbuffer:
	JSR wait_for_screen		; Wait to see if the screen is done it's previous transfer
	SET PUSH, Y			; Save the Y register
	SHL Y, 5			; Multiply by 32 - since each line is 32 chars wide
	ADD Y, X			; Add the start of the video buffer to the offset
	IOW 0x0008, Y			; Scroll the screen to line Y
	SET Y, POP
	SET PC, POP

Video Example 5: Modifying character glyphs

The character glyph concept is fairly frequently used in machines of the era -- we're just moving the glyph buffer from memory into the video device. Here's an example of how you might use that:

wait_for_io:
	SET PUSH, A			; Save A
wfioloop:
	IOR A, X			; Read the DMA transfer/action status from location X
	IFN A, 0x00			; If it's doing something...
		SUB PC, 3		;    ...jump back to wfioloop

	SET A, POP			; Restore A and return
	SET PC, POP

glyphs:
	DAT	0x4143, 0xff81, 0x8181, 0x8181, 0x81ff, 0xff81, 0xc3a5, 0xa5c3, 0x81ff, 0xff81, 0x8199, 0x9981, 0x81ff

set_checkbox_glyphs:
	SET PUSH, X			; Save X
	SET X, 0x000b			; Write Character Glyphs Port

	JSR wait_for_io			; Wait for any I/O to complete on port 0x0004
	IOW X, glyphs			; Set the 'A', 'B', and 'C' glyphs
	
	SET X, POP			; Restore X and return
	SET PC, POP

As soon as the transfer completes for this operation, all "A" characters on the screen will change to an empty box, while all "B" characters will change to a box with an 'X' through it, and all "C" characters will change to a box with a dot in the center. (Generally useful glyphs to use for unchecked, checked, and partially checked checkboxes.)

Restoring them is just as easy:

restore_checkbox_glyphs:
	SET PUSH, X			; Save X
	SET X, 0x000c			; Restore Character Glyphs Port

	JSR wait_for_io			; Wait for any I/O to complete on port 0x0005
	IOW X, 0x4143			; Restore the 'A' - 'C' glyphs
	
	SET X, POP			; Restore X and return
	SET PC, POP

You could just as easily restore the entire range of glyphs by doing IOW, X, 0x00ff instead, but depending on the time the video buffer takes to do that, it could be more efficient to restore just the glyphs you changed.

Back to Top

Floppy Disk Device

Notch has stated that he plans to have the computers running using something like 1.44 MB Floppy Disk Drives. So we obviously need a disk device. Here is a very simple one - which simply has Seek, Read, and Write capabilities, and a couple of registers which might be useful if Notch decides to support multiple drives or hard drives in the future.

I/O LocationNameBehavior ClassDescription
0x0010 Seek Disk Sector Action Writing to the Seek location will cause the currently selected drive to seek to the sector number given in the written value. It might take some time for the seek to complete. Reading from this location will return the current sector number it's pointing to, with the high bit set if it is currently processing a seek operation, and cleared if it is no longer seeking. If you Seek while a seek is currently in progress for that disk device, the previous Seek is canceled and a new Seek begins. If you Seek to a sector which does not exist on the device, it will seek to the closest sector to that number - thus you could determine how many sectors a drive has by Seeking to 0x7fff, and when it finishes, checking to see what the Seek register says. If you Seek on a device which has no media inserted, it will Seek to sector 0. If you do a Seek while a sector Read or Write is in progress, the Seek will not happen until after that Read or Write completes, and the status will indicate Seek is in progress during that time.
0x0011 Read Disk Sector DMA Transfer Writing an address to the Read Disk Sector location will trigger a Read from currently selected disk at the current Seek sector and a DMA transfer of that disk sector's contents to the DCPU-16 memory at the given address. If you read while a Seek is in progress, it will terminate the Seek immediately, and read whatever sector it happens to be pointing to - which is indeterminate. The read process will take some time to complete, at least 1 cycle per memory word transferred, plus the time to actually read a floppy disk sector, which could be significant. Reading from this location will return a status word which will be zero if the transfer is complete, 1 if the transfer is in progress, 2 if there is no floppy in the drive, 3 if a bad sector error occured, or something else if some other read error occurred. After a successful read, but before the read status is updated, the Seek sector address will be incremented if not at the end of the disk. If a Read is scheduled while a Write is in progress, it will not interrupt the Write, but the Read will not begin until the Write completes. (And will thus read the sector *following* the written sector, since Write increments the Seek pointer.)
0x0012 Write Disk Sector DMA Transfer Writing an address to the Write Disk Sector location will trigger a Write to currently selected disk at the current Seek sector and a DMA transfer of DCPU-16 memory at the given address to write out to that disk sector. If you write while a Seek is in progress, it will terminate the Seek immediately, and write whatever sector it happens to be pointing to - which is indeterminate. The write process will take some time to complete, at least 1 cycle per memory word transferred, plus the time to actually write a floppy disk sector, which could be longer than a read. Reading from this location will return a status word which will be zero if the transfer is complete, 1 if the transfer is in progress, , 2 if there is no floppy in the drive, 3 if a bad sector error occured, 4 if it fails because the floppy has been write protected, or something else if any other write error occurred. After a successful write, but before the write status is updated, the Seek sector address will be incremented if not at the end of the disk. If a Write is scheduled while a Read is in progress, it will not interrupt the Read, but the Write will not begin until the Read completes. (And will thus write to the sector *following* the read sector, since Read increments the Seek pointer.)
0x0013 Drive Select Control Writing to this address changes the selected drive. By default, 0 is the boot drive - but Notch might allow us to add more drives. Reading from this returns the currently selected Drive number. Note that drives are independent - so you could be reading from one and writing to another at the same time. If you write a Drive number to this location which does not exist, it will be set to zero. Drives operate completely independently, so you can have concurrent reads and writes happening on different drives.
0x0014 Drive Status Control Reading from this control register returns status and information about the drive. The low 7 bits are the sector size, in multiples of 256 words. (I'm limiting it to 7 bits because 8 would allow for a 64K sector, which isn't readable in the DCPU-16 currently without overwriting the running code completely...) With a seek range of 32K sectors (due to the high bit being used for status), this allows a device with 256 word sectors to have up to 8MW of data, and up to a theoretical maximum of 1GB of data - not that we'll ever actually get that high any time soon. I predict that a floppy disk will have 1440 or 2880 minimal size sectors, based on Notch's tweets so far. Other bits in this register are status bits to be defined - the only ones I'll define right now is an error bit, which will get set if a Seek, Read, Write, or Drive Select error occurs, and a write-protected bit, which indicates the inserted floppy has been write protected. The Writing to this address will simply set the appropriate bits, except for the sector size bits which are fixed.

Disk Drive Example 1: Reading and writing a sector

Here are example functions for seeking, reading and writing sectors. (These are blocking functions for example purposes, but if you're careful, you can do it without blocking) Note that some of these use the wait_for_io() function from Video Example 5, above.:

; seek_sector - Seeks to the sector given in Y.
;    Returns zero if successful in A, otherwise it returns 1, and Y is changed to
;    the actual sector Seek says we're pointing to.
;
seek_sector:
	SET PUSH, X			; Save X
	SET X, 0x0010			; Seek Disk Location
	
	IOR A, X			; Read the current sector
	IFE A, Y			; If it's already the specified sector...
		SET PC, seek_complete	; Return that the seek was completed successfully
	
	IOW X, Y			; Start SEEKing to the requested sector
	
seek_wait:
	IOR A, X			; Read the current seek sector
	IFG A, 0x7fff			; If it's greater than 0x7fff (ie. has the high bit set)
		SET PC, seek_wait	; Loop on checking the seek sector again

	IFE A, Y			; If the final seek sector was the specified sector...
		SET PC, seek_complete	; Return that the seek was completed successfully
		
	SET Y, A			; Return a Seek error
	SET A, 1
	SET X, POP
	SET PC, POP
	
seek_complete:
	SET A, 0			; Return Seek Success
	SET X, POP
	SET PC, POP
	
; rw_sector - Seeks to the sector given in Y, and reads or writes that sector into the address given in Z.
;    The decision of whether it reads or writes is based on the I/O location specified in X - if it's 0x0011,
;    it will read, if it's 0x0012, it will write.
;    Returns zero if successful in A.   If unsuccessful due to a seek error, A will be 1 and Y will be
;    changed to the current seek sector.   If unsuccessful due to a read error, A will be 2 or more.
;
rw_sector:
	IOR A, X			; Read the current read/write status
	IFE A, 1			; If it's a 1, we're still doing a read/write...
		SET PC, rw_sector	; So loop on checking the read/write status again

	JSR seek_sector			; Seek to the desired sector
	IFE A, 0			; If the seek was successful...
		JMP do_rw_sector	; Do the actual read/write
	SET PC, POP			; Otherwise pass the seek error back up
	
do_rw_sector:
	IOW X, Z			; Start the read/write operation, storing the sector at the address in Z

rw_wait:
	IOR A, X			; Read the current read/write status
	IFE A, 1			; If it's a 1, we're still doing a read/write...
		SET PC, rw_wait		; So loop on checking the read/write status again
	
	SET PC, POP			; At this point, A is either 0 (success) or >1 (failed with error) so
					;    cleanup and return
	
; read_sector - Seeks to the sector given in Y, and reads from that sector into the address given in Z
;    Returns zero if successful in A.   If unsuccessful due to a seek error, A will be 1 and Y will be
;    changed to the current seek sector.   If unsuccessful due to a read error, A will be 2 or more.
;
read_sector:
	SET PUSH, X			; Save X
	SET X, 0x0011			; Read Disk Location
	JSR rw_sector			; Do the write
	SET X, POP
	SET PC, POP

; write_sector - Seeks to the sector given in Y, and writes to that sector from the address given in Z
;    Returns zero if successful in A.   If unsuccessful due to a seek error, A will be 1 and Y will be
;    changed to the current seek sector.   If unsuccessful due to a write error, A will be 2 or more.
;
write_sector:
	SET PUSH, X			; Save X
	SET X, 0x0012			; Write Disk Location
	JSR rw_sector			; Do the write
	SET X, POP
	SET PC, POP

Disk Drive Example 2: Copying a floppy disk from one drive to another

Here's a more elaborate example, which copies an entire disk from Drive 0 to Drive 1, taking advantage of the ability to read from one drive into one buffer and write to another drive from another buffer, at the same time:

; copy_drive0_to_drive1 - Copies the disk in drive0 to drive1, using the buffer in X, which has a length in Y,
;	as a transfer buffer.  The transfer buffer must be two time the size of a sector (or higher, although it
;	doesn't help to
;	be any higher as it only uses two buffers...)
;
;	Returns A = 0 if successful, A = 1 if the buffer given is too small, A = 2 if the disks aren't the same
;	size, or A = 3 if some kind of seek, read, or write error occurred
;
copy_drive0_to_drive1:

;
; Initial setup - we use almost every register here, and need to restore all but A, which
; is the return value
;
	SET PUSH, B			; Save B - we'll use it as a scratch register
	SET PUSH, C			; Save C - we'll use it to store the status flag mask for the Seek
	SET PUSH, I			; Save I - we'll use it as drive 0 next sector number
	SET PUSH, J			; Save J - we'll use it as drive 1 next sector number
	SET PUSH, X			; Save X - that's our first buffer pointer
	SET PUSH, Y			; Save Y - we'll use it as the second buffer pointer
	SET PUSH, Z			; Save Z - we'll use it as the maximum sector

;
; Determine the size of buffer we're going to need to hold two sectors in memory
; and return an error if the buffer given is too small.
;
cdd_checkbuffer:
	IOW, 0x0013, 0			; Drive select zero
	IOR A, 0x0014			; Get drive status
	AND A, 0x007f			; Get drive sector size
	SHL A, 0x09			; Multiply by 512 to get 2x sector word size
	IFN A, Y			; If the target buffer is the same size
		IFG Y, A		;    as 2x the sector or greater than that
			SET PC, cdd_checksector	;    continue.  (Yes this looks weird - work it out. :D )
			
cdd_buffertoosmall:
	SET A, 1			; If we get here, then the buffer is too small, so return an error
	SET PC, cdd_ret

;
; Make sure both drives are using the same sector size.   If not, return an error
;		
cdd_checksector:
	IOW, 0x0013, 1			; Drive select one
	IOR B, 0x0014			; Get drive status
	AND B, 0x007f			; Get drive sector size
	SHL B, 0x09			; Multiply by 256 to get 2x sector word size (to match A)
	IFE A, B			; If the two drives are using the same sector size, continue
		SET PC, cdd_setbuffers
		
cdd_notsamesize:
	SET A, 2			; If we get here, then the disks aren't the same size, so return an error
	SET PC, cdd_ret
	
;
; At this point we know the sector size, so we can set up Y to be the second buffer
; pointer.   We need two buffers because we'll be reading to one while writing from
; the other.
;
cdd_setbuffers:
	SHR A, 1			; Shift A back to get the actual sector size
	SET Y, X			; Set up the second buffer pointer in Y
	ADD Y, A			;    by adding the sector size to X

;
; Seek both drives to the end of the disk.   This is used to determine the number of
; sectors on each drive.
;	
cdd_seekend:	
	IOW, 0x0013, 0			; Drive select zero
	IOW, 0x0010, 0x7fff		; Seek drive 0 to the end of disk
	IOW, 0x0013, 1			; Drive select one
	IOW, 0x0010, 0x7fff		; Seek drive 1 to the end of disk
	SET C, 0x8000			; Bit Mask for the seek status bit

;
; Now wait for both seeks to complete, and check to make sure they have
; the same number of sectors, and store that number in Z for later comparison.
; If not, return an error.   Because a seek while a read or write operation
; is in progress will wait until that operation completes, this also guarantees
; that there was no outstanding read or write on either drive.
;	
cdd_waitsize:
	IOW, 0x0013, 0			; Drive select zero
	IOR, A, 0x0010			; Get seek status
	AND A, C			; Get the status bit
	IOW, 0x0013, 1			; Drive select one
	IOR, B, 0x0010			; Get seek status
	AND B, C			; Get the status bit
	IFE A, 0			; If either bit is set
		IFN B, 0		;    loop again.  (Hint - IFx like this works out to 
			SET PC, cdd_waitsize	;    if !(cond1) OR (cond2) action)

	IFE A, B			; If the numbers of sectors aren't the same,
		SET PC, cdd_notsamesize	;	report that the disks aren't the same size
		
	SET Z, A			; Save the highest sector number in Z, so we know when the
					;    copy will be done later.

;
; Seek both disks back to the beginning, and set up the B and C registers to reflect
; the next read and next write sector, respectively						
;
cdd_seekstart:	
	IOW, 0x0013, 1			; Drive select one
	IOW, 0x0010, 0x7fff		; Seek drive 1 to the start of disk
	IOW, 0x0013, 0			; Drive select zero
	IOW, 0x0010, 0x7fff		; Seek drive 0 to the start of disk
	SET I, 0			; Set the target sector for drive 0 to 0
	SET J, 0			; Set the target sector for drive 1 to 0

	
;
; Okay, now we can get into the main copy loop.   If we're done with the copy, because the next
; read sector is beyond the end of the disk, exit.   Otherwise run through the copy process for one
; sector, increment the sector counters, and swap the buffer pointers we're using for the next pass,
; and loop again.
;
cdd_mainloop:
	IFG I, Z			; Are we done? Go to exit
		SET PC, cdd_done
		
	JSR cdd_do_copy			; The meat of the copy process
	IFE A, 0			; If it didn't have an error, continue
		SET PC, cdd_nextsector	

cdd_diskerror:
	SET A, 3			; If we get here, then we had a disk error, so return an error
	SET PC, cdd_ret		

cdd_nextsector:
	ADD I, 1			; Increment the seek read counter.  The read operation should have incremented the Seek position on disk when it finished.
	ADD J, 1			; Increment the seek write counter.  The write operation will increment the Seek position on disk when done.
	SET PUSH, X			; 3 instruction SWAP X,Y using the stack
	SET X, Y
	SET Y, POP
	SET PC, cdd_nextsector	; Loop

;
; We're not quite done at this point -- the last write is still running, so we have to wait for that
; to finish before we can return success.
;
cdd_done:	
	IOR A, 0x0012			; Get write status on drive one
	IFE A, 1			; If it's still writing...
		SET PC, cdd_done	;    wait until it's done
	IFN A, 0			; If it had a write error - report a disk error
		SET PC, cdd_diskerror

;
; Finally, clean up and return success
;
	SET A, 0
	
cdd_ret:
	SET Z, POP
	SET Y, POP
	SET X, POP
	SET J, POP
	SET I, POP
	SET C, POP
	SET B, POP
	SET PC, POP

	
; cdd_do_copy - This is the inner disk copy routine.   It's job is to read sector I from drive 0,
;	into the buffer specified in X, and write it to sector J of drive 1.   The drives are expected
;	to already have Seeked to those sectors, and it uses I and J to check before reading or writing
;	from/to them.   Register C is expected to contain 0x8000 which is a mask bit for checking to see
;	if a Seek operation is in progress.
;
;	At entry, Seeks on either drive, or a write on drive 1 from a previous call to this function might
;	be in progress, but the 0 drive is assumed to have completed it's previous read operation.
;
;	The function waits for the previous seek to be done on drive 0, confirms at that point that the
;	drive is pointing to the sector given in register I, then begins the read operation for that sector,
;	into the buffer pointed to by X.   It will then wait until any previous write or seek on drive 1 is
;	complete, and the read it just scheduled on drive 0 to be complete as well, and when all three are
;	complete and it confirms drive one is pointing to the sector given in register J, it writes out the
;	buffer pointed to by X into that sector.   It returns without waiting for that write to complete,
;	leaving drive select set to 1.
;
;	By alternating buffers, you can call this immediately after to start the read/write process on the
;	next sector, as it will be reading sector N+1 on drive 0 while writing sector N on drive 1. 
;
;	If a disk error is detected on any Seek, Read or Write, it stops the copy and returns 1 in register A,
;	otherwise it returns 0 in register A.

cdd_do_copy:
	IOW, 0x0013, 0			; Drive select zero	
	
;
; At this point, we know we need to read a sector.   But we could be in the middle of
; a seek right now, so we need to check for that.   If we end up in a situation where the drive isn't
; seeking, but isn't pointing to the expected sector, then we need to stop and return an error.
;		
cdd_waitseekread:
	IOR A, 0x0010			; Get seek status on drive zero
	IFE A, I			; If we're pointing to the right sector (which after the initial seek ought to be always true)
		SET PC, cdd_read	;    go to reading the sector
	AND A, C			; If the high bit is still set...
	IFN A, 0			;    loop back until the seek completes
		SET PC, cdd_waitseekread

cdd_copyerror:
	SET A, 1			; If we get here, then return a disk error
	SET PC, POP

;
; We know we've done a successful seek now, so read the sector into the currently selected buffer,
; and increment the next seek location for reads.
;
cdd_read:
	IOW 0x0011, X			; Start reading from the current drive 0 sector to the first buffer

;
; Are we ready to write yet?  First, there might be an outstanding write going on, so lets check for
; that and wait for it to complete before we continue, and report an error if it fails.
;	
	IOW, 0x0013, 1			; Drive select one
cdd_waitwrite:
	IOR A, 0x0012			; Get write status on drive one
	IFE A, 1			; If it's still writing...
		SET PC, cdd_waitwrite	;    wait until it's done
	IFN A, 0			; If it had a write error - report a disk error
		SET PC, cdd_copyerror

;
; If we weren't writing, we might be seeking - especially first time through.  If we end up in a situation
; where the drive isn't seeking, but isn't pointing to the expected sector, then we need to stop and report
; a disk error.
;				
cdd_waitseekwrite:
	IOR A, 0x0010			; Get seek status on drive one
	IFE A, J			; If we're pointing to the right sector (which after the initial seek ought to be always true)
		SET PC, cdd_waitread	;    go to waiting for the read to finish
	AND A, C			; If the high bit is still set...
	IFN A, 0			;    loop back until the seek completes
		SET PC, cdd_waitseekwrite
	SET PC, cdd_copyerror		; Otherwise report a disk error

;
; The previous write (if any) is done, and we're ready to write - but we could still be reading.  If we are,
; then we need to wait for the read to finish, and should report a disk error if it fails.
;
cdd_waitread:
	IOW 0x0013, 0			; Drive select drive 0
	
cdd_waitread1:
	IOR A, 0x0011			; Get read status on drive zero
	IFE A, 1			; If it's still reading...
		SET PC, cdd_waitread1	;    wait until it's done
	IFN A, 0			; If it had a write error - report a disk error
		SET PC, cdd_copyerror
		
;
; At this point, the next sector has been read in the currently selected buffer, so write it to the disk,
; and increment the next seek location for writes.
;
cdd_write:
	SET 0x0013, 1		; Drive select drive 1
	IOW 0x0012, X		; Start writing to the current drive 1 sector from the first buffer

;
; We don't need to wait for the write to complete here.  Just return and let it start the next loop on
; reading
;
	SET A, 0
	SET PC, POP

Back to Top

Rom Boot Device

We obviously need some Boot procedure on power on or hard reset, and in keeping with the design, I'm proposing that there be a minimalistic boot rom, the contents of which are copied to address 0 in memory, prior to starting the CPU. Here's an example of what might go in there to force a boot off of the floppy disk in drive 0:

Rom Boot Example 1: A simple floppy disk boot loader

;
; romboot - This code, located at address zero, is copied from ROM into RAM at power up time,
; and boots off of a floppy inserted into drive 0.   If no floppy exists in drive 0, or we have
; a seek or read failure, it will fail by going into an infinite loop.
;
@0x0000:

;
; First, we want to copy the main boot code to an address above 0x8000, since
; when we read the first sector of the boot drive, it will overwrite this initial
; code
;
; Yes, this is an inefficient copy routine, but it's written for clarity more than speed
;
	SET X, bootstart	; Start of actual boot code
	SET Y, bootend		; End of actual boot code
	SET Z, 0xf000		; Relocation target
	
bootcopy:
	SET [Z], [X]		; Relocate a word of the boot code
	ADD X, 1		; Increment X and Z
	ADD Z, 1
	IFN X, Y		; If we aren't done relocating it...
		SUB PC, 5 	;    loop back to bootcopy.
		
	SET PC, 0xf000		; Jump to bootstart at it's relocated address
	
;
; Since everything from this point on is relocated, it cannot use any fixed addresses,
; and all branches must be relative
;
bootstart:
	
	IOW 0x13, 0		; Drive select drive 0
	SET Y, 0x8000		; Seek status bit
	
;
; Seek to sector 0.  Technically, it's probably already there.
;
	IOW 0x10, 0		; Seek to sector 0
bootseek:
	IOR X, 0x10		; Get seek sector status
	IFE X, 0		; If we're done
		ADD PC,4	;    jump to bootread
	AND X, Y
	IFE X, 0		; If we're not seeking and not on sector 1,
		SUB PC, 1	;    fail by going into an infinite loop
	SUB PC, 7		; Loop back to bootseek

;
; Read sector 0 into address 0, and wait for it to complete
;
bootread:
	IOW	0x11, 0		; Read sector 0 into address 0
	
bootwait:
	IOR X, 0x0011		; Get read sector status
	IFE X, 1		; If we're still reading
		SUB PC, 3	;    jump back to bootwait
	IFN X, 0		; If we weren't successful
		SUB PC, 1	;    fail by going into an infinite loop

;
; At this point, the read is done, so jump to the start of the read sector
; to continue the boot.
;
bootdone:
	SET PC, 0

Back to Top

Firmware Device with ROM boot

Another approach to booting would be to define a Firmware device in combination with the ROM boot. In that case, we would have the option of booting from either the Firmware (which could in fact be a more comprehensive boot loader/memory test with screen display), or it could still boot from the floppy if the firmware is empty or has been disabled externally through a jumper switch. (Useful for when you need to fix buggy firmware.)

The firmware is accessible through I/O locations just like any other device. The locations defined include:

I/O LocationNameBehavior ClassDescription
0x00f0 Firmware Status Control The firmware status word is used to determine if the firmware is enabled, to enable writing the firmware, and to determine it's size. The low 8 bits is the firmware size in banks of 1K words, read only, allowing for firmware sizes from 1K words words to 256K words. Other bits include:
  • 0x0100 - Set this to 1 to enable writing to the firmware (if it's not write protected)
  • 0x0200 - Firmware enabled if 1, disabled if 0 (read only)
  • 0x0400 - Firmware write protected if 1, writable if 0 (read only)
0x00f1 Firmware Bank Select Control This selects the bank (1KW block) of firmware being accessed. This is thus a value from 0-N, where N is the firmware size in the Firmware Status register - 1. No check for range is done here.
0x00f2 Firmware Read DMA Transfer When you IOW an address to this I/O location, it will DMA transfer a single 1KW bank of Firmware to that address from the currently selected Bank select location. Reading this will return:
  • 0 - If no transfer is happening and/or the last transfer was successful
  • 1 - If transfer is in progress
  • 2 - If read failed due to the firmware being disabled
  • 3 - If read failed due to the bank select being out of range.
0x00f3 Firmware Write DMA Transfer When you IOW an address to this I/O location, it will DMA transfer a single 1KW bank of memory from the DCPU-16, burning it to the currently selected bank of Firmware. Reading this will return:
  • 0 - If no transfer is happening and/or the last transfer was successful
  • 1 - If transfer is in progress
  • 2 - If write failed due to the firmware being disabled
  • 3 - If write failed due to the bank select being out of range.
  • 4 - If write failed due to the firmware being write protected.
  • 5 - If write failed due to writing not being enabled.

Rom Boot Example 2: A simple firmware OR floppy disk boot loader

This is what a Boot ROM would look like if there was a Firmware device present as well. It tries to boot the first 1KW block of firmware (which could load additional firmware blocks), and if it can't do that, it will fall back and boot from floppy. Presumably, any Firmware which was loaded would have it's own floppy boot mechanism, relocated in much the same way.

;
; romboot - This code, located at address zero, is copied from ROM into RAM at power up time,
; and boots off of firmware if it's available, enabled, and not empty, otherwise it attempts to
; boot from a floppy inserted into drive 0.   If in that case, no floppy exists in drive 0, or
; we have a seek or read failure, it will fail by going into an infinite loop.
;
@0x0000:

;
; First, we want to copy the main boot code to an address above 0x8000, since
; when we read firmware or the floppy, it will overwrite this initial code
;
; Yes, this is an inefficient copy routine, but it's written for clarity more than speed
;
	SET X, bootstart	; Start of actual boot code
	SET Y, bootend		; End of actual boot code
	SET Z, 0xf000		; Relocation target
	
bootcopy:
	SET [Z], [X]		; Relocate a word of the boot code
	ADD X, 1		; Increment X and Z
	ADD Z, 1
	IFN X, Y		; If we aren't done relocating it...
		SUB PC, 5 	;    loop back to bootcopy.
		
	SET PC, 0xf000		; Jump to bootstart at it's relocated address
	
;
; Since everything from this point on is relocated, it cannot use any fixed addresses,
; and all branches must be relative
;
bootstart:

	SET Y, 0x00f1		; FW Bank Select register
	SET Z, 0x00f2		; FW Read register
	
	SET X, 0		; Clear out X in case firmware doesn't exist
	IOR	X, 0xf0		; Get the firmware status register
	AND X, 0x0200		; Check to see if the firmware is enabled
	IFE X, 0		; If it's not enabled or doesn't exist, jump to bootfloppy
		ADD PC, 10

	IOW Y, 0		; Select Firmware bank 0
	IOW Z, 0		; and read it to address 0
	
bootfwread:
	IOR X, Z		; Check to see if the read is done
	IFE X, 1		; If not, loop back to bootfwread
		SUB PC, 3
	IFN X, 0		; If it had an error, jump to bootfloppy
		ADD PC, 3
	IFE [X], 0		; If the firmware was empty address 0 will be zero, so jump to bootfloppy
		ADD PC, 1
	
bootfwdone:
	SET PC, 0		; Jump to the loaded firmware
	
;
; Firmware couldn't load, so boot from floppy
;
bootfloppy:
	IOW 0x13, 0		; Drive select drive 0
	SET Y, 0x8000		; Seek status bit
	
;
; Seek to sector 0.  Technically, it's probably already there.
;
	IOW 0x10, 0		; Seek to sector 0
bootseek:
	IOR X, 0x10		; Get seek sector status
	IFE X, 0		; If we're done
		ADD PC,4	;    jump to bootread
	AND X, Y
	IFE X, 0		; If we're not seeking and not on sector 1,
		SUB PC, 1	;    fail by going into an infinite loop
	SUB PC, 7		; Loop back to bootseek

;
; Read sector 0 into address 0, and wait for it to complete
;
bootread:
	IOW	0x11, 0		; Read sector 0 into address 0
	
bootwait:
	IOR X, 0x0011		; Get read sector status
	IFE X, 1		; If we're still reading
		SUB PC, 3	;    jump back to bootwait
	IFN X, 0		; If we weren't successful
		SUB PC, 1	;    fail by going into an infinite loop

;
; At this point, the read is done, so jump to the start of the read sector
; to continue the boot.
;
bootdone:
	SET PC, 0

Back to Top

Frequently Asked Questions

What if Notch doesn't expand the opcodes by removing a bit from the A operand?

The IOW/IOR scheme above assumes we have at least two more dual-operand opcodes available. If that doesn't happen, we need to define alternate opcodes to accomplish the same task. Here are two approaches:

Alternate Opcodes 1: I/O Message Location Register "M"

In this mechanism, we define a new register on the DCPU-16 - an I/O location register, which I'll call 'M'. Then we have two special opcodes, similar to the JSR opcode, for setting and getting the location, and two special opcodes for writing out to the I/O location and reading from it:

	STM iolocation			; Sets the I/O Message Location register M to iolocation
	GTM target			; Gets the current value of the I/O Message Location register and stores it in target
	IOW value			; Writes the given value to the I/O Message Location M
	IOR target			; Reads the I/O Message Location M and stores it in target

Alternate Opcodes 2: Extended special opcodes

In this mechanism, we create a two-word format for special opcodes which can support up to three operands and an additional 4 bit constant, as follows:

	aaaaaaoooooo0000		- Normal special opcode format, where certain opcodes require...
	bbbbbbccccccdddd		- ...a second word which defines operands b, c, and a 4 bit added constant d

Given this format, we can define a single opcode to do I/O:

	IO target,source,location,direction

...where:

This format can be used for a variety of other useful special instructions. For example, I originally came up with this format for three operand buffer instructions:

	CPY target,source,len		; Copies the len length buffer from source to target, @ 1 or 2 cycles per word copied
	FIL target,source,len		; Fills the len length buffer at target with the source value, @ 1 cycles per word filled
	FND target,source,len,svr	; Searches for source in target buffer, for up to len words, leaving the offset found or
					;    len if not found, in the register specified by svr, @ 1 cycle per word checked

Back to Top

Document Change History

Back to Top