Storage controller
When you stop running your Hybrix program ("turn off" the virtual machine), all of its RAM memory is lost. The Hybrix storage controller provides a way to save some information so that it can be accessed the next time that your program is run. The design allows for various storage devices such as hard drives, removable disks, and persistent memory. Each attached device is called a storage volume.
For now, the only supported device is the flash chip that is built into each ROM cartridge. (Hard drives with filesystems will be added in the future.)
Flash chip
The flash chip holds only 4,096 bytes of information, just enough for saving user settings or a player's progress in a video game.
The Hybrix computer is a virtual system being simulated by the website; in the same way, the flash "chip" is not real electronics. The flash chip data is actually saved to your web browser's local storage repository. This repository is not synced between computers, nor even between different web browsers (Firefox, Safari, Chrome, etc.) on the same computer. In fact, if you open the Hybrix website in a private browsing tab, the flash chip storage will be lost when you close that tab.
Hybrix defines a flash chip ID to distinguish different chips in the web browser's repository. When you are running your program in the Hybrix development environment, the debugger gives each project its own flash chip: the flash chip ID is your project identifier from the website URL, something like project/677b1d8921209da8149741ca.
If you have exported a cartridge ROM file (PNG image) to be viewed using the Hybrix ROM Player, then the flash chip ID is a hash function of the cartridge memory. In other words, if you redo the export of your cartridge ROM file with even one byte changed, it will be considered a different flash chip. On the other hand, updating the cartridge title or screenshot has no effect.
Using the storage controller
A sector is a chunk of bytes that are read or written together. Each storage device has a fixed sector size and a fixed count of sectors that are accessed by number. The flash chip has 4 sectors that are each 1,024 bytes in size, for a total of 4,096 bytes.
A program uses memory-mapped input/output (MMIO) to interact with the storage controller. There are three basic commands: get information about a volume, read one sector, and write one sector. The sector gets copied to or from your program's memory using direct memory access (DMA), which simply means that your program can be doing other things while the transfer is happening.
The DRIVE_STATUS byte
The IO::DRIVE_STATUS byte orchestrates the operations. Status byte values in the range 0 – 127 are called ready codes; these codes indicate that the previous operation has finished, and the device is now ready to accept a new command. Your program starts a new command by writing 128 (START COMMAND) to the IO::DRIVE_STATUS byte. This is the only value that your program is allowed to write, and only when the storage controller is ready.
Drive controller commands are issued in six steps:
- Check if ready: Your program must NOT write to
IO::DRIVE_STATUSwhile its value is greater than 127. To check that the controller is not busy performing an earlier command, read fromIO::DRIVE_STATUSin a loop until a ready code (0 – 127) is returned. - Set command: Specify your new command in the
IO::DRIVE_COMMANDbyte. For example, to get information about a volume, set the command byte to 1 (GET INFO). - Set volume: Use
IO::DRIVE_VOLUMEto specify the volume number of the device. For now, the only available choice is 0 which is the flash chip. In the future, more devices will be introduced. - Set parameters: Depending on the command, set other parameters such as
IO::DRIVE_SECTOR_NUMandIO::DRIVE_DMA_ADDRESS. See the next section for details. - Start the command: After setting up these parameters, start the operation by writing 128 to
IO::DRIVE_STATUS. When the controller sees this,IO::DRIVE_STATUSmay change to 129 indicating that the operation is underway. For a READ SECTOR command, new data should begin appearing at theIO::DRIVE_DMA_ADDRESSlocation after some time. - Await the result: Repeatedly read from
IO::DRIVE_STATUSuntil a ready code (0 – 127) is returned. A value of 0 indicates success. Values 8 or higher indicate various errors.
Command byte values
1 – GET INFO
Set IO::DRIVE_COMMAND to 1 for the GET INFO command. It queries the following details about a given IO::DRIVE_VOLUME number:
- Whether a device is attached: If not,
IO::DRIVE_STATUSreturns error code 8 or 9 (see below). - The type of device (and sector size): Reported in
IO::DRIVE_INFO_TYPE. Each device type will have a predetermined sector size that can be found in the documentation. - The total number of sectors: Reported in
IO::DRIVE_INFO_TOTAL_SECTORS. For example, the flash chip has 4 sectors total.
Enumerating volumes: In the future, when multiple volumes are supported, a program may wish to query the list of available volumes. It is not necessary perform the GET INFO command for all 256 possibilities of IO::DRIVE_VOLUME. If IO::DRIVE_STATUS returns 9 (DEVICE NOT PRESENT, REACHED END), then it's not necessary to check any higher volume numbers. Whereas if the status byte is 8 (DEVICE NOT PRESENT), then although that volume number has no device, you do need to continue looping.
2 – READ SECTOR
Set IO::DRIVE_COMMAND to 2 for the READ SECTOR command. It reads one sector from a volume. The sector number is specified in IO::DRIVE_SECTOR_NUM. The sector bytes are copied to the memory address specified by IO::DRIVE_DMA_ADDRESS.
For the flash chip, reading one sector takes approximately 10 ms.
Warning: Your program must ensure the
IO::DRIVE_DMA_ADDRESSbuffer is at least as large as one sector; otherwise, memory corruption can occur. For the flash chip, one sector is 1,024 bytes.
3 – WRITE SECTOR
Set IO::DRIVE_COMMAND to 3 for the WRITE SECTOR command. It writes one sector to a volume. The sector number is specified in IO::DRIVE_SECTOR_NUM. The sector bytes are copied from the memory address specified by IO::DRIVE_DMA_ADDRESS.
For the flash chip, writing one sector takes approximately 120 ms, considerably slower than reading.
Code sample: saving
Suppose we are making a video game, and we want to save the "high score." The data looks like this:
CLASS GAME_STATE
# THE PLAYER'S HIGH SCORE
VAR SCORE: INT
# THE PLAYER'S NAME
VAR NAME: STRING
END CLASS
This object is not suitable for IO::DRIVE_DMA_ADDRESS because (1) it is smaller than the flash chip's sector size of 1,024 bytes, and (2) the NAME field is actually a pointer to a separate memory block that is a BYTE[] array (STRING in this code). The solution is to serialize the data, which means packing it into a single chunk of bytes. We can declare the serialized structure like this:
CLASS FLASH_SECTOR # 1024 BYTES TOTAL
VAR SCORE: INT # 4 BYTES
INSET NAME: ^BYTE[SIZE 32] # 32 BYTES
INSET PADDING: ^BYTE[SIZE 988] # 1024 - 32 - 4
END CLASS
Recall that the INSET storage directive causes the NAME and PADDING arrays to be located directly in the memory for FLASH_SECTOR. The padding ensures that the allocated memory is 1,024 bytes total.
Below is a function that saves GAME_STATE to the flash memory, which our game should do whenever a new high score is achieved. First it serializes GAME_STATE into FLASH_SECTOR, then it performs the 6 steps described above for a WRITE SECTOR command.
MODULE MAIN
# CALL THIS FUNCTION TO SAVE THE HIGH SCORE
FUNC SAVE(GAME_STATE: GAME_STATE)
VAR FLASH_SECTOR: FLASH_SECTOR
NEW FLASH_SECTOR() -> FLASH_SECTOR
# SERIALIZE GAME_STATE
GAME_STATE.SCORE -> FLASH_SECTOR.SCORE
VAR I: PAIR
0 -> I
LOOP
IF I >= GAME_STATE.NAME.SIZE
DO DROP
GAME_STATE.NAME[I] -> FLASH_SECTOR.NAME[I]
TO_PAIR(I + 1) -> I
END LOOP
# 1. WAIT FOR ANY PREVIOUS OPERATION TO FINISH
LOOP
IF IO::DRIVE_STATUS < 128
DO DROP
END LOOP
# 2. SET THE NEW COMMAND
3 -> IO::DRIVE_COMMAND # 3 = WRITE SECTOR
# 3. FOR ROM CARTRIDGES, IT'S SAFE TO ASSUME THE FLASH CHIP IS VOLUME 0.
0 -> IO::DRIVE_VOLUME
# 4. WRITE FLASH_SECTOR TO SECTOR 0
# IMPORTANT: BE CAREFUL THAT THE GARBAGE COLLECTOR WON'T FREE FLASH_SECTOR
# DURING THE OPERATION.
0 -> IO::DRIVE_SECTOR_NUM
TO_ADDRESS(FLASH_SECTOR) -> IO::DRIVE_DMA_ADDRESS
# 5. START THE OPERATION BY SETTING IO::DRIVE_STATUS TO 128
128 -> IO::DRIVE_STATUS
# 6. WAIT FOR COMPLETION
LOOP
IF IO::DRIVE_STATUS < 128
DO DROP
END LOOP
IF IO::DRIVE_STATUS <> 0
DO KERNEL::FAIL("ERROR WRITING FLASH CHIP")
END FUNC
. . .
END MODULE
Nice work! You just implemented a device driver.
Code sample: loading
Below is a similar function that loads GAME_STATE from the flash memory, to be called when the program first starts running. First it performs the 6 steps described above for a READ SECTOR command. Then it deserializes FLASH_SECTOR into GAME_STATE.
MODULE MAIN
. . .
FUNC LOAD(GAME_STATE: GAME_STATE)
VAR FLASH_SECTOR: FLASH_SECTOR
NEW FLASH_SECTOR() -> FLASH_SECTOR
# 1. WAIT FOR ANY PREVIOUS OPERATION TO FINISH
LOOP
IF IO::DRIVE_STATUS < 128
DO DROP
END LOOP
# 2. SET THE NEW COMMAND
2 -> IO::DRIVE_COMMAND # 2 = READ SECTOR
# 3. FOR ROM CARTRIDGES, IT'S SAFE TO ASSUME THE FLASH CHIP IS VOLUME 0.
0 -> IO::DRIVE_VOLUME
# 4. READ SECTOR 0 INTO FLASH_SECTOR
# IMPORTANT: BE CAREFUL THAT THE GARBAGE COLLECTOR WON'T FREE FLASH_SECTOR
# DURING THE OPERATION.
0 -> IO::DRIVE_SECTOR_NUM
TO_ADDRESS(FLASH_SECTOR) -> IO::DRIVE_DMA_ADDRESS
# 5. START THE OPERATION BY SETTING IO::DRIVE_STATUS TO 128
128 -> IO::DRIVE_STATUS
# 6. WAIT FOR COMPLETION
LOOP
IF IO::DRIVE_STATUS < 128
DO DROP
END LOOP
IF IO::DRIVE_STATUS <> 0
DO KERNEL::FAIL("ERROR WRITING FLASH CHIP")
# DESERIALIZE GAME_STATE
FLASH_SECTOR.SCORE -> GAME_STATE.SCORE
VAR NAME: STRING
NEW BYTE[](32) -> NAME
NAME.RESIZE(32)
VAR I: PAIR, C: BYTE
0 -> I
LOOP
FLASH_SECTOR.NAME[I] -> C
IF C = 0
DO DROP
C -> NAME[I]
TO_PAIR(I + 1) -> I
END LOOP
NAME.RESIZE(I)
NAME -> GAME_STATE.NAME
END FUNC
END MODULE
I/O definitions
MODULE IO
. . .
# STORAGE CONTROLLER
#
# (SEE STATUS CODES BELOW)
VAR DRIVE_STATUS: BYTE LOCATED AT $D0_0050
# 1 = GET INFO
# 2 = READ SECTOR
# 3 = WRITE SECTOR
VAR DRIVE_COMMAND: BYTE LOCATED AT $D0_0051
# 0 = FLASH CHIP
VAR DRIVE_VOLUME: BYTE LOCATED AT $D0_0052
# 1 = FLASH CHIP, SECTOR SIZE IS 1024, 4 SECTORS TOTAL PER CARTRIDGE
VAR DRIVE_INFO_TYPE: BYTE LOCATED AT $D0_0053
VAR DRIVE_INFO_TOTAL_SECTORS: INT LOCATED AT $D0_0054
VAR DRIVE_SECTOR_NUM: INT LOCATED AT $D0_0058
VAR DRIVE_DMA_ADDRESS: INT LOCATED AT $D0_005C
# STATUS CODES REPORTED BY: TRANSMIT_STATUS, RECEIVE_STATUS, DRIVE_STATUS
#
# 0 = DONE
# 8 = ERROR: DEVICE NOT PRESENT
# 9 = ERROR: DEVICE NOT PRESENT, REACHED END
# 10 = ERROR: INVALID COMMAND
# 11 = ERROR: INVALID SECTOR NUMBER
# 12 = ERROR: MEMORY FAULT
#
# 128 = START COMMAND - WRITTEN BY PROGRAM TO START AN OPERATION
# 129 = BUSY - WILL CHANGE TO A "READY CODE" (< 128) AFTER THE WORK IS DONE
. . .
END MODULE