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

Contents

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:

A Clean Memory Map - By implementing this system, we eliminate fixed assignments within the DCPU-16 memory map. The memory map starts out completely open, and it's up to the individual programmers to decide what maps where.

- By implementing this system, we eliminate fixed assignments within the DCPU-16 memory map. The memory map starts out completely open, and it's up to the individual programmers to decide what maps where. Ease of Implementation - This system effectively isolates peripheral devices from the CPU. They can run entirely in their own server thread or in the main thread at their own pace, without being aware of what the CPU is doing except when an explicit I/O instruction is run - and even then it does not need to respond immediately.

- This system effectively isolates peripheral devices from the CPU. They can run entirely in their own server thread or in the main thread at their own pace, without being aware of what the CPU is doing except when an explicit I/O instruction is run - and even then it does not need to respond immediately. Simplicity and Consistency - The system defines a very simple and consistent mechanism for performing I/O, and is easily expandable to support more devices.

- The system defines a very simple and consistent mechanism for performing I/O, and is easily expandable to support more devices. Speed of Operations - This system creates a faster I/O mechanism for many things, without adding more load to the server emulation.

- This system creates a faster I/O mechanism for many things, without adding more load to the server emulation. Asynchronous - all lengthy I/O requests in this scheme can be done entirely asynchronously.

- all lengthy I/O requests in this scheme can be done entirely asynchronously. Interrupt agnostic - Notch has stated he doesn't intend to implement interrupts. This scheme does not require them, but they could trivially, and optionally, be added to them if he does.

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:

Additional Opcodes - One of the prerequisites of this system is we will need additional opcodes. Most of this spec is written assuming that the A operand will be reduced to 5 bits, thus freeing up one bit to allow an additional 16 dual-operand opcodes, of which we are using two here. A lot of people are assuming this will happen based on this Twitter conversation from Notch. If he doesn't make this change, then I have alternative schemes using the single operand opcodes (like JSR) described in the FAQ, here.

- One of the prerequisites of this system is we will need additional opcodes. Most of this spec is written assuming that the A operand will be reduced to 5 bits, thus freeing up one bit to allow an additional 16 dual-operand opcodes, of which we are using two here. A lot of people are assuming this will happen based on this Twitter conversation from Notch. If he doesn't make this change, then I have alternative schemes using the single operand opcodes (like JSR) described in the FAQ, here. Clearing the Memory Map - Technically, the 1.1 spec doesn't say anything about the memory map having any fixed locations, but much of the example code Notch has been publishing defines a video buffer, a keyboard buffer, and possibly some other things in various fixed areas of the memory map. While he's not updated the spec to reflect this, it was watching this ongoing fragmentation of the limited memory space of the DCPU-16 which drove me to suggesting this design. For it to work to it's best, it will be important for all peripheral devices he's currently mapping to DCPU-16 memory ranges to switch to the new scheme and remove their memory mapping, leaving the memory map clear for all uses, anywhere.

Back to Top

The I/O System

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

A pair of opcodes to Read and Write a single word to an I/O location 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:

Write 10 to the Disk Seek I/O location - This tells the disk device to Seek to that sector, but it might take some time.

- This tells the disk device to Seek to that sector, but it might take some time. Loop on Reading the Disk Seek I/O location - For action-type I/O locations, reading the location returns a status word which tells you if the action you requested is complete or not. So we loop on reading the status word until the Seek is complete.

- For action-type I/O locations, reading the location returns a status word which tells you if the action you requested is complete or not. So we loop on reading the status word until the Seek is complete. Write 0x2000 to the Disk Read I/O location - This tells the disk device to Read the sector at the current disk Seek position, and DMA copy it's data directly into memory at the given address. As with Seek, it might take some time.

- This tells the disk device to Read the sector at the current disk Seek position, and DMA copy it's data directly into memory at the given address. As with Seek, it might take some time. Loop on Reading the Disk Read I/O location - As with the Seek location, this returns a status word indicating whether the Read is complete, and we loop until the status indicates it's done. The contents of that memory are undefined until that happens.

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:

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. 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. 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. 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: The Video Buffer Transfer location - updates the video device's on-board buffer with a copy of the buffer specified on the DCPU-16.

The Disk Read location - Reads a sector on the disk into a block of memory

The Disk Write location - Writes a block of memory to a sector on the disk

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 Location Name Behavior Class Description 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 Location Name Behavior Class Description 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.

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 Location Name Behavior Class Description 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 Location Name Behavior Class Description 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:

target - is the target where a read value would be stored

source - is the source value to be written

location - is the I/O location

direction - is the direction of I/O, which can be: 0x1 - For Writing only - the target operand is ignored in this case 0x2 - For Reading only - the source operand is ignored in this case 0x3 - For a Write followed by a Read



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

04/15/12 - Created document from original proposal, changing INP/OUTP to IOR/IOW, rewriting, reformatting, and expanding considerably, including full examples for each device.

Back to Top