[SC64][SW] Added access to the SD card via USB interface (#90)

This commit is contained in:
Mateusz Faderewski 2024-09-29 20:58:52 +02:00 committed by GitHub
parent 3146cc8c99
commit 74e20cb8cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1897 additions and 203 deletions

View File

@ -18,6 +18,7 @@
- Dedicated open source menu written specifically for this flashcart - [N64FlashcartMenu](https://github.com/Polprzewodnikowy/N64FlashcartMenu) - Dedicated open source menu written specifically for this flashcart - [N64FlashcartMenu](https://github.com/Polprzewodnikowy/N64FlashcartMenu)
- Enhanced [UltraCIC_C](https://github.com/jago85/UltraCIC_C) emulation with automatic region switching and programmable seed/checksum values - Enhanced [UltraCIC_C](https://github.com/jago85/UltraCIC_C) emulation with automatic region switching and programmable seed/checksum values
- PC app for communicating with flashcart (game/save data upload/download, feature enable control and debug terminal) - PC app for communicating with flashcart (game/save data upload/download, feature enable control and debug terminal)
- Access to the SD card via USB interface with the use of the PC app
- [UNFLoader](https://github.com/buu342/N64-UNFLoader) support - [UNFLoader](https://github.com/buu342/N64-UNFLoader) support
- Initial programming via UART header or dedicated JTAG/SWD interfaces - Initial programming via UART header or dedicated JTAG/SWD interfaces
- Software and firmware updatable via USB interface - Software and firmware updatable via USB interface

View File

@ -1,50 +1,38 @@
- [First time setup on PC](#first-time-setup-on-pc) - [First time setup](#first-time-setup)
- [Firmware backup/update](#firmware-backupupdate) - [Standalone mode (Running menu and games on the N64)](#standalone-mode-running-menu-and-games-on-the-n64)
- [Running menu in standalone mode](#running-menu-in-standalone-mode) - [Developer mode (Uploading ROMs from the PC, and more)](#developer-mode-uploading-roms-from-the-pc-and-more)
- [Uploading game and/or save from PC](#uploading-game-andor-save-from-pc) - [Uploading game and/or save from PC](#uploading-game-andor-save-from-pc)
- [Downloading save to PC](#downloading-save-to-pc) - [Downloading save to PC](#downloading-save-to-pc)
- [Running 64DD games from PC](#running-64dd-games-from-pc) - [Running 64DD games from PC](#running-64dd-games-from-pc)
- [Direct boot option](#direct-boot-option) - [Direct boot option](#direct-boot-option)
- [Debug terminal on PC](#debug-terminal-on-pc) - [Debug terminal on PC](#debug-terminal-on-pc)
- [Firmware backup/update](#firmware-backupupdate)
- [LED blink patters](#led-blink-patters) - [LED blink patters](#led-blink-patters)
--- ---
## First time setup on PC # First time setup
**Windows platform: replace `./sc64deployer` in examples below with `sc64deployer.exe`** ## Standalone mode (Running menu and games on the N64)
1. Download the latest deployer tool (`sc64-deployer-{os}-{version}.{ext}`) and firmware (`sc64-firmware-{version}.bin`) from GitHub releases page
2. Extract deployer tool package contents to a folder and place firmware file inside it
3. Connect SC64 device to your computer with USB type C cable
4. Run `./sc64deployer list` to check if device is detected in the system
5. Update SC64 firmware to the latest version with `./sc64deployer firmware update sc64-firmware-{version}.bin`
6. Run `./sc64deployer info` to check if update process finished successfully and SC64 is detected correctly
---
## Firmware backup/update
Keeping SC64 firmware up to date is highly recommended.
`sc64deployer` application is tightly coupled with specific firmware versions and will error out when it detects unsupported firmware version.
To download and backup current version of the SC64 firmware run `./sc64deployer firmware backup sc64-firmware-backup.bin`
To update SC64 firmware run `./sc64deployer firmware update sc64-firmware-{version}.bin`
To print firmware metadata run `./sc64deployer firmware info sc64-firmware-{version}.bin`
---
## Running menu in standalone mode
Menu, as known from 64drive or EverDrive-64, is developed in another repository: [N64FlashcartMenu](https://github.com/Polprzewodnikowy/N64FlashcartMenu). Menu, as known from 64drive or EverDrive-64, is developed in another repository: [N64FlashcartMenu](https://github.com/Polprzewodnikowy/N64FlashcartMenu).
Download latest version from [here](https://github.com/Polprzewodnikowy/N64FlashcartMenu/releases) and put `sc64menu.n64` file in the root directory of the SD card. Download latest version from [here](https://github.com/Polprzewodnikowy/N64FlashcartMenu/releases) and put `sc64menu.n64` file in the root directory of the SD card.
Additionally, follow the instructions in the N64FlashcartMenu repository for more information about thr SD card setup and extra functionality.
When N64 is powered on menu is automatically loaded from the SD card. Supported file system formats are FAT32 and exFAT. When N64 is powered on menu is automatically loaded from the SD card. Supported file system formats are FAT32 and exFAT.
--- ---
## Uploading game and/or save from PC ## Developer mode (Uploading ROMs from the PC, and more)
**Windows platform: replace `./sc64deployer` in examples below with `sc64deployer.exe`**
1. Download the latest deployer tool (`sc64-deployer-{os}-{version}.{ext}`) from the GitHub releases page
2. Extract deployer tool package contents to a folder
3. Connect SC64 device to your computer with USB type C cable
4. Run `./sc64deployer list` to check if device is detected in the system
5. Follow instructions below for specific use cases
### Uploading game and/or save from PC
`./sc64deployer upload path_to_rom.n64 --save-type eeprom4k --save path_to_save.sav` `./sc64deployer upload path_to_rom.n64 --save-type eeprom4k --save path_to_save.sav`
@ -53,18 +41,14 @@ Application will try to autodetect used save type so explicitly setting save typ
Check included help in the application to list available save types. Check included help in the application to list available save types.
Arguments `--save-type` and/or `--save` can be omitted if game doesn't require any save or you want to start with fresh save file. Arguments `--save-type` and/or `--save` can be omitted if game doesn't require any save or you want to start with fresh save file.
--- ### Downloading save to PC
## Downloading save to PC
`./sc64deployer download save path_to_save.sav` `./sc64deployer download save path_to_save.sav`
Replace `path_to_save.sav` with appropriate value. Replace `path_to_save.sav` with appropriate value.
Command will raise error when no save type is currently enabled in the SC64 device. Command will raise error when no save type is currently enabled in the SC64 device.
--- ### Running 64DD games from PC
## Running 64DD games from PC
64DD games require DDIPL ROM and disk images. 64DD games require DDIPL ROM and disk images.
To run disk game type `./sc64deployer 64dd path_to_ddipl.n64 path_to_disk_1.ndd path_to_disk_2.ndd`. To run disk game type `./sc64deployer 64dd path_to_ddipl.n64 path_to_disk_1.ndd path_to_disk_2.ndd`.
@ -79,31 +63,38 @@ Make sure retail and development disks formats aren't mixed together.
If disk game supports running in conjunction with cartridge game then `--rom path_to_rom.n64` argument can be added to command above. If disk game supports running in conjunction with cartridge game then `--rom path_to_rom.n64` argument can be added to command above.
N64 will boot cartridge game instead of 64DD IPL. N64 will boot cartridge game instead of 64DD IPL.
--- ### Direct boot option
## Direct boot option
If booting game through included bootloader isn't a desired option then flashcart can be put in special mode that omits this step. If booting game through included bootloader isn't a desired option then flashcart can be put in special mode that omits this step.
Pass `--direct` option in `upload` or `64dd` command to disable bootloader during boot and console reset. Pass `--direct` option in `upload` or `64dd` command to disable bootloader during boot and console reset.
This option is useful only for very specific cases (e.g. testing custom IPL3 or running SC64 on top of GameShark). This option is useful only for very specific cases (e.g. testing custom IPL3 or running SC64 on top of GameShark).
TV type cannot be forced when direct boot mode is enabled. TV type cannot be forced when direct boot mode is enabled.
--- ### Debug terminal on PC
## Debug terminal on PC
`sc64deployer` application supports UNFLoader protocol and has same functionality implemented as aforementioned program. `sc64deployer` application supports UNFLoader protocol and has same functionality implemented as aforementioned program.
Type `./sc64deployer debug` to activate it. Type `./sc64deployer debug` to activate it.
### Firmware backup/update
Keeping SC64 firmware up to date is strongly recommended.
`sc64deployer` application is tightly coupled with specific firmware versions and will error out when it detects unsupported firmware version.
To download and backup current version of the SC64 firmware run `./sc64deployer firmware backup sc64-firmware-backup.bin`
To update SC64 firmware run `./sc64deployer firmware update sc64-firmware-{version}.bin`
To print firmware metadata run `./sc64deployer firmware info sc64-firmware-{version}.bin`
--- ---
## LED blink patters # LED blink patters
LED on SC64 board can blink in certain situations. Most of them during normal use are related to SD card access. Here's list of blink patterns: LED on SC64 board can blink in certain situations. Most of them during normal use are related to SD card access. Here's list of blink patterns:
| Pattern | Meaning | | Pattern | Meaning |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Nx [Short ON - Short OFF] | SD card access is in progress (initialization or data read/write) or save writeback is in progress | | Irregular | SD card access is in progress (initialization or data read/write) or save writeback was finished |
| Nx [Medium ON - Long OFF] | CIC region did not match, please power off console and power on again | | Nx [Medium ON - Long OFF] | CIC region did not match, please power off console and power on again |
| 2x [Very short ON - Short OFF] | Pattern used during firmware update process, it means that specific part of firmware has started programming | | 2x [Very short ON - Short OFF] | Pattern used during firmware update process, it means that specific part of firmware has started programming |
| 10x [Very short ON - Very short OFF] | Firmware has been successfully updated | | 10x [Very short ON - Very short OFF] | Firmware has been successfully updated |
@ -112,4 +103,4 @@ LED on SC64 board can blink in certain situations. Most of them during normal us
Nx means that blink count is varied. Nx means that blink count is varied.
LED blinking on SD card access can be disabled through `sc64deployer` application. LED blinking on SD card access can be disabled through `sc64deployer` application.
Please refer to included help for option to change the LED behavior. Please refer to the included help for option to change the LED behavior.

View File

@ -25,9 +25,9 @@ This mapping is used internally by FPGA/μC and when accessing flashcart from US
| Flash [1] | `0x0400_0000` | 16 MiB | RW/R | Flash | | Flash [1] | `0x0400_0000` | 16 MiB | RW/R | Flash |
| Data buffer | `0x0500_0000` | 8 kiB | RW | BlockRAM | | Data buffer | `0x0500_0000` | 8 kiB | RW | BlockRAM |
| EEPROM | `0x0500_2000` | 2 kiB | RW | BlockRAM | | EEPROM | `0x0500_2000` | 2 kiB | RW | BlockRAM |
| 64DD buffer | `0x0500_2800` | 256 bytes | RW | BlockRAM | | 64DD/MCU buffer | `0x0500_2800` | 1 kiB | RW | BlockRAM |
| FlashRAM buffer [2] | `0x0500_2900` | 128 bytes | R | BlockRAM | | FlashRAM buffer [2] | `0x0500_2C00` | 128 bytes | R | BlockRAM |
| N/A [3] | `0x0500_2980` | to `0x07FF_FFFF` | R | N/A | | N/A [3] | `0x0500_2C80` | to `0x07FF_FFFF` | R | N/A |
- Note [1]: Flash memory region `0x04E0_0000` - `0x04FD_FFFF` is write protected as it contains N64 bootloader. This section can be overwritten only via firmware update process. - Note [1]: Flash memory region `0x04E0_0000` - `0x04FD_FFFF` is write protected as it contains N64 bootloader. This section can be overwritten only via firmware update process.
- Note [2]: Due to BlockRAM usage optimization this section is read only. - Note [2]: Due to BlockRAM usage optimization this section is read only.
@ -53,8 +53,8 @@ This mapping is used when accessing flashcart from N64 side.
| ROM shadow [7] | `0x1FFC_0000` | 128 kiB | R | `0x04FE_0000` | Flash | mem bus | SC64 register access is enabled | | ROM shadow [7] | `0x1FFC_0000` | 128 kiB | R | `0x04FE_0000` | Flash | mem bus | SC64 register access is enabled |
| Data buffer | `0x1FFE_0000` | 8 kiB | RW | `0x0500_0000` | Block RAM | mem bus | SC64 register access is enabled | | Data buffer | `0x1FFE_0000` | 8 kiB | RW | `0x0500_0000` | Block RAM | mem bus | SC64 register access is enabled |
| EEPROM | `0x1FFE_2000` | 2 kiB | RW | `0x0500_2000` | Block RAM | mem bus | SC64 register access is enabled | | EEPROM | `0x1FFE_2000` | 2 kiB | RW | `0x0500_2000` | Block RAM | mem bus | SC64 register access is enabled |
| 64DD buffer [8] | `0x1FFE_2800` | 256 bytes | RW | `0x0500_2800` | Block RAM | mem bus | SC64 register access is enabled | | 64DD/MCU buffer [8] | `0x1FFE_2800` | 1 kiB | RW | `0x0500_2800` | Block RAM | mem bus | SC64 register access is enabled |
| FlashRAM buffer [8] | `0x1FFE_2900` | 128 bytes | R | `0x0500_2900` | Block RAM | mem bus | SC64 register access is enabled | | FlashRAM buffer [8] | `0x1FFE_2C00` | 128 bytes | R | `0x0500_2C00` | Block RAM | mem bus | SC64 register access is enabled |
| SC64 registers | `0x1FFF_0000` | 28 bytes | RW | N/A | Flashcart Interface | reg bus | SC64 register access is enabled | | SC64 registers | `0x1FFF_0000` | 28 bytes | RW | N/A | Flashcart Interface | reg bus | SC64 register access is enabled |
- Note [1]: 64DD IPL share SDRAM memory space with ROM (last 4 MiB minus 128 kiB for saves). Write access is always disabled for this section. - Note [1]: 64DD IPL share SDRAM memory space with ROM (last 4 MiB minus 128 kiB for saves). Write access is always disabled for this section.

View File

@ -4,28 +4,28 @@
## N64 commands ## N64 commands
| id | name | arg0 | arg1 | rsp0 | rsp1 | description | | id | name | arg0 | arg1 | rsp0 | rsp1 | description |
| --- | --------------------- | ------------- | ------------ | ---------------- | -------------- | ---------------------------------------------------------- | | --- | --------------------- | ------------- | ------------ | ---------------- | -------------- | ------------------------------------------------------------ |
| `v` | **IDENTIFIER_GET** | --- | --- | identifier | --- | Get flashcart identifier `SCv2` | | `v` | **IDENTIFIER_GET** | --- | --- | identifier | --- | Get flashcart identifier `SCv2` |
| `V` | **VERSION_GET** | --- | --- | major/minor | revision | Get flashcart firmware version | | `V` | **VERSION_GET** | --- | --- | major/minor | revision | Get flashcart firmware version |
| `c` | **CONFIG_GET** | config_id | --- | --- | current_value | Get config option | | `c` | **CONFIG_GET** | config_id | --- | --- | current_value | Get config option |
| `C` | **CONFIG_SET** | config_id | new_value | --- | previous_value | Set config option and get previous value | | `C` | **CONFIG_SET** | config_id | new_value | --- | previous_value | Set config option and get previous value |
| `a` | **SETTING_GET** | setting_id | --- | --- | current_value | Get persistent setting option | | `a` | **SETTING_GET** | setting_id | --- | --- | current_value | Get persistent setting option |
| `A` | **SETTING_SET** | setting_id | new_value | --- | --- | Set persistent setting option | | `A` | **SETTING_SET** | setting_id | new_value | --- | --- | Set persistent setting option |
| `t` | **TIME_GET** | --- | --- | time_0 | time_1 | Get current RTC value | | `t` | **TIME_GET** | --- | --- | time_0 | time_1 | Get current RTC value |
| `T` | **TIME_SET** | time_0 | time_1 | --- | --- | Set new RTC value | | `T` | **TIME_SET** | time_0 | time_1 | --- | --- | Set new RTC value |
| `m` | **USB_READ** | pi_address | length | --- | --- | Receive data from USB to flashcart | | `m` | **USB_READ** | pi_address | length | --- | --- | Receive data from USB to flashcart |
| `M` | **USB_WRITE** | pi_address | length/type | --- | --- | Send data from from flashcart to USB | | `M` | **USB_WRITE** | pi_address | length/type | --- | --- | Send data from from flashcart to USB |
| `u` | **USB_READ_STATUS** | --- | --- | read_status/type | length | Get USB read status and type/length | | `u` | **USB_READ_STATUS** | --- | --- | read_status/type | length | Get USB read status and type/length |
| `U` | **USB_WRITE_STATUS** | --- | --- | write_status | --- | Get USB write status | | `U` | **USB_WRITE_STATUS** | --- | --- | write_status | --- | Get USB write status |
| `i` | **SD_CARD_OP** | pi_address | operation | --- | return_data | Perform special operation on SD card | | `i` | **SD_CARD_OP** | pi_address | operation | --- | return_data | Perform special operation on the SD card |
| `I` | **SD_SECTOR_SET** | sector | --- | --- | --- | Set starting sector for next SD card R/W operation | | `I` | **SD_SECTOR_SET** | sector | --- | --- | --- | Set starting sector for next SD card R/W operation |
| `s` | **SD_READ** | pi_address | sector_count | --- | --- | Read sectors from SD card to flashcart | | `s` | **SD_READ** | pi_address | sector_count | --- | --- | Read sectors from the SD card to flashcart memory space |
| `S` | **SD_WRITE** | pi_address | sector_count | --- | --- | Write sectors from flashcart to SD card | | `S` | **SD_WRITE** | pi_address | sector_count | --- | --- | Write sectors from the flashcart memory space to the SD card |
| `D` | **DISK_MAPPING_SET** | pi_address | table_size | --- | --- | Set 64DD disk mapping for SD mode | | `D` | **DISK_MAPPING_SET** | pi_address | table_size | --- | --- | Set 64DD disk mapping for SD mode |
| `w` | **WRITEBACK_PENDING** | --- | --- | pending_status | --- | Get save writeback status (is write queued to the SD card) | | `w` | **WRITEBACK_PENDING** | --- | --- | pending_status | --- | Get save writeback status (is write queued to the SD card) |
| `W` | **WRITEBACK_SD_INFO** | pi_address | --- | --- | --- | Load writeback SD sector table and enable it | | `W` | **WRITEBACK_SD_INFO** | pi_address | --- | --- | --- | Load writeback SD sector table and enable it |
| `K` | **FLASH_PROGRAM** | pi_address | length | --- | --- | Program flash with bytes loaded into data buffer | | `K` | **FLASH_PROGRAM** | pi_address | length | --- | --- | Program flash with bytes loaded into data buffer |
| `p` | **FLASH_WAIT_BUSY** | wait | --- | erase_block_size | --- | Wait until flash ready / get block erase size | | `p` | **FLASH_WAIT_BUSY** | wait | --- | erase_block_size | --- | Wait until flash ready / get block erase size |
| `P` | **FLASH_ERASE_BLOCK** | pi_address | --- | --- | --- | Start flash block erase | | `P` | **FLASH_ERASE_BLOCK** | pi_address | --- | --- | --- | Start flash block erase |
| `%` | **DIAGNOSTIC_GET** | diagnostic_id | --- | --- | value | Get diagnostic data | | `%` | **DIAGNOSTIC_GET** | diagnostic_id | --- | --- | value | Get diagnostic data |

View File

@ -45,6 +45,22 @@
- [`data` (data)](#data-data-1) - [`data` (data)](#data-data-1)
- [`X`: **AUX\_WRITE**](#x-aux_write) - [`X`: **AUX\_WRITE**](#x-aux_write)
- [`arg0` (data)](#arg0-data) - [`arg0` (data)](#arg0-data)
- [`i`: **SD\_CARD\_OP**](#i-sd_card_op)
- [`arg0` (address)](#arg0-address-2)
- [`arg1` (operation)](#arg1-operation)
- [`response` (result/status)](#response-resultstatus)
- [Available SD card operations](#available-sd-card-operations)
- [SD card status](#sd-card-status)
- [`s`: **SD\_READ**](#s-sd_read)
- [`arg0` (address)](#arg0-address-3)
- [`arg1` (sector\_count)](#arg1-sector_count)
- [`data` (sector)](#data-sector)
- [`response` (result)](#response-result)
- [`S`: **SD\_WRITE**](#s-sd_write)
- [`arg0` (address)](#arg0-address-4)
- [`arg1` (sector\_count)](#arg1-sector_count-1)
- [`data` (sector)](#data-sector-1)
- [`response` (result)](#response-result-1)
- [`D`: **DD\_SET\_BLOCK\_READY**](#d-dd_set_block_ready) - [`D`: **DD\_SET\_BLOCK\_READY**](#d-dd_set_block_ready)
- [`arg0` (error)](#arg0-error) - [`arg0` (error)](#arg0-error)
- [`W`: **WRITEBACK\_ENABLE**](#w-writeback_enable) - [`W`: **WRITEBACK\_ENABLE**](#w-writeback_enable)
@ -152,30 +168,33 @@ Available packet IDs are listed in the [asynchronous packets](#asynchronous-pack
## Supported commands ## Supported commands
| id | name | arg0 | arg1 | data | response | description | | id | name | arg0 | arg1 | data | response | description |
| --- | ----------------------------------------------- | ------------ | ------------- | ---- | ---------------- | -------------------------------------------------------------- | | --- | ----------------------------------------------- | ------------ | ------------- | ------ | ---------------- | -------------------------------------------------------------- |
| `v` | [**IDENTIFIER_GET**](#v-identifier_get) | --- | --- | --- | identifier | Get flashcart identifier `SCv2` | | `v` | [**IDENTIFIER_GET**](#v-identifier_get) | --- | --- | --- | identifier | Get flashcart identifier `SCv2` |
| `V` | [**VERSION_GET**](#v-version_get) | --- | --- | --- | version | Get flashcart firmware version | | `V` | [**VERSION_GET**](#v-version_get) | --- | --- | --- | version | Get flashcart firmware version |
| `R` | [**STATE_RESET**](#r-state_reset) | --- | --- | --- | --- | Reset flashcart state (CIC params and config options) | | `R` | [**STATE_RESET**](#r-state_reset) | --- | --- | --- | --- | Reset flashcart state (CIC params and config options) |
| `B` | [**CIC_PARAMS_SET**](#b-cic_params_set) | cic_params_0 | cic_params_1 | --- | --- | Set CIC emulation parameters (disable/seed/checksum) | | `B` | [**CIC_PARAMS_SET**](#b-cic_params_set) | cic_params_0 | cic_params_1 | --- | --- | Set CIC emulation parameters (disable/seed/checksum) |
| `c` | [**CONFIG_GET**](#c-config_get) | config_id | --- | --- | config_value | Get config option | | `c` | [**CONFIG_GET**](#c-config_get) | config_id | --- | --- | config_value | Get config option |
| `C` | [**CONFIG_SET**](#c-config_set) | config_id | config_value | --- | --- | Set config option | | `C` | [**CONFIG_SET**](#c-config_set) | config_id | config_value | --- | --- | Set config option |
| `a` | [**SETTING_GET**](#a-setting_get) | setting_id | --- | --- | setting_value | Get persistent setting option | | `a` | [**SETTING_GET**](#a-setting_get) | setting_id | --- | --- | setting_value | Get persistent setting option |
| `A` | [**SETTING_SET**](#a-setting_set) | setting_id | setting_value | --- | --- | Set persistent setting option | | `A` | [**SETTING_SET**](#a-setting_set) | setting_id | setting_value | --- | --- | Set persistent setting option |
| `t` | [**TIME_GET**](#t-time_get) | --- | --- | --- | time | Get current RTC value | | `t` | [**TIME_GET**](#t-time_get) | --- | --- | --- | time | Get current RTC value |
| `T` | [**TIME_SET**](#t-time_set) | time_0 | time_1 | --- | --- | Set new RTC value | | `T` | [**TIME_SET**](#t-time_set) | time_0 | time_1 | --- | --- | Set new RTC value |
| `m` | [**MEMORY_READ**](#m-memory_read) | address | length | --- | data | Read data from specified memory address | | `m` | [**MEMORY_READ**](#m-memory_read) | address | length | --- | data | Read data from specified memory address |
| `M` | [**MEMORY_WRITE**](#m-memory_write) | address | length | data | --- | Write data to specified memory address | | `M` | [**MEMORY_WRITE**](#m-memory_write) | address | length | data | --- | Write data to specified memory address |
| `U` | [**USB_WRITE**](#u-usb_write) | type | length | data | N/A | Send data to be received by app running on N64 (no response!) | | `U` | [**USB_WRITE**](#u-usb_write) | type | length | data | N/A | Send data to be received by app running on N64 (no response!) |
| `X` | [**AUX_WRITE**](#x-aux_write) | data | --- | --- | --- | Send small auxiliary data to be received by app running on N64 | | `X` | [**AUX_WRITE**](#x-aux_write) | data | --- | --- | --- | Send small auxiliary data to be received by app running on N64 |
| `D` | [**DD_SET_BLOCK_READY**](#d-dd_set_block_ready) | error | --- | --- | --- | Notify flashcart about 64DD block readiness | | `i` | [**SD_CARD_OP**](#i-sd_card_op) | address | operation | --- | result/status | Perform special operation on the SD card |
| `W` | [**WRITEBACK_ENABLE**](#w-writeback_enable) | --- | --- | --- | --- | Enable save writeback through USB packet | | `s` | [**SD_READ**](#s-sd_read) | address | sector_count | sector | result | Read sectors from the SD card to flashcart memory space |
| `p` | **FLASH_WAIT_BUSY** | wait | --- | --- | erase_block_size | Wait until flash ready / Get flash block erase size | | `S` | [**SD_WRITE**](#s-sd_write) | address | sector_count | sector | result | Write sectors from the flashcart memory space to the SD card |
| `P` | **FLASH_ERASE_BLOCK** | address | --- | --- | --- | Start flash block erase | | `D` | [**DD_SET_BLOCK_READY**](#d-dd_set_block_ready) | error | --- | --- | --- | Notify flashcart about 64DD block readiness |
| `f` | **FIRMWARE_BACKUP** | address | --- | --- | status/length | Backup firmware to specified memory address | | `W` | [**WRITEBACK_ENABLE**](#w-writeback_enable) | --- | --- | --- | --- | Enable save writeback through USB packet |
| `F` | **FIRMWARE_UPDATE** | address | length | --- | status | Update firmware from specified memory address | | `p` | **FLASH_WAIT_BUSY** | wait | --- | --- | erase_block_size | Wait until flash ready / Get flash block erase size |
| `?` | **DEBUG_GET** | --- | --- | --- | debug_data | Get internal FPGA debug info | | `P` | **FLASH_ERASE_BLOCK** | address | --- | --- | --- | Start flash block erase |
| `%` | **DIAGNOSTIC_GET** | --- | --- | --- | diagnostic_data | Get diagnostic data | | `f` | **FIRMWARE_BACKUP** | address | --- | --- | status/length | Backup firmware to specified memory address |
| `F` | **FIRMWARE_UPDATE** | address | length | --- | status | Update firmware from specified memory address |
| `?` | **DEBUG_GET** | --- | --- | --- | debug_data | Get internal FPGA debug info |
| `%` | **DIAGNOSTIC_GET** | --- | --- | --- | diagnostic_data | Get diagnostic data |
--- ---
@ -223,7 +242,7 @@ _This command does not require arguments or data._
_This command does not send response data._ _This command does not send response data._
This command is used to reset most of the config options to default state (same as on power-up). This command is used to reset most of the config options to default state (same as on power-up).
Additionally, CIC emulation is enabled and 6102/7101 seed/checksum values are set. Additionally, CIC emulation is enabled, 6102/7101 seed/checksum values are set and SD card lock is released.
--- ---
@ -467,6 +486,110 @@ This command puts 32 bits of data to the AUX register accessible from the N64 si
--- ---
### `i`: **SD_CARD_OP**
**Perform special operation on the SD card**
#### `arg0` (address)
| bits | description |
| -------- | ----------- |
| `[31:0]` | Address |
#### `arg1` (operation)
| bits | description |
| -------- | ----------- |
| `[31:0]` | Operation |
#### `response` (result/status)
| offset | type | description |
| ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `0` | uint32_t | Operation result (valid values are listed in the [sd_error_t](../sw/controller/src/sd.h) enum) |
| `4` | uint32_t | SD card status (always returned and valid regardless of the SD card operation result) |
This command performs special operation on the SD card. When operation result is not `SD_OK`, then `ERR` packet is returned.
PC and N64 cannot use the SD card interface at the same time. Lock mechanism is implemented to prevent data corruption.
SD card access is locked when PC or N64 requests SD card initialization, and unlocked when the card is uninitialized.
Lock is also released when certain conditions occur - when N64 side lock is active then it is released when the console is powered off
and save writeback operation is not pending. PC side lock is released when USB interface is in reset state, or the USB cable is unplugged.
#### Available SD card operations
| operation | description |
| --------- | ---------------------------------------------------------------------------------------------- |
| `0` | Init SD card |
| `1` | Deinit SD card |
| `2` | Get SD card status |
| `3` | Get SD card info (loads CSD and CID registers to a specified address, data length is 32 bytes) |
| `4` | Turn on byte swap |
| `5` | Turn off byte swap |
#### SD card status
| bits | description |
| -------- | -------------------------------------------------------------------------- |
| `[31:5]` | _Unused_ |
| `[4]` | `0` - Byte swap disabled, `1` - Byte swap enabled (valid when initialized) |
| `[3]` | `0` - 25 MHz clock, `1` - 50 MHz clock (valid when initialized) |
| `[2]` | `0` - Byte addressed, `1` - Sector addressed (valid when initialized) |
| `[1]` | `0` - SD card not initialized, `1` - SD card initialized |
| `[0]` | `0` - SD card not inserted, `1` - SD card inserted |
---
### `s`: **SD_READ**
**Read sectors from the SD card to flashcart memory space**
#### `arg0` (address)
| bits | description |
| -------- | ----------- |
| `[31:0]` | Address |
#### `arg1` (sector_count)
| bits | description |
| -------- | ------------ |
| `[31:0]` | Sector count |
#### `data` (sector)
| offset | type | description |
| ------ | -------- | --------------- |
| `0` | uint32_t | Starting sector |
#### `response` (result)
| offset | type | description |
| ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `0` | uint32_t | Operation result (valid values are listed in the [sd_error_t](../sw/controller/src/sd.h) enum) |
This command reads sectors from the SD card to a specified memory address. When operation result is not `SD_OK`, then `ERR` packet is returned.
---
### `S`: **SD_WRITE**
**Write sectors from the flashcart memory space to the SD card**
#### `arg0` (address)
| bits | description |
| -------- | ----------- |
| `[31:0]` | Address |
#### `arg1` (sector_count)
| bits | description |
| -------- | ------------ |
| `[31:0]` | Sector count |
#### `data` (sector)
| offset | type | description |
| ------ | -------- | --------------- |
| `0` | uint32_t | Starting sector |
#### `response` (result)
| offset | type | description |
| ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `0` | uint32_t | Operation result (valid values are listed in the [sd_error_t](../sw/controller/src/sd.h) enum) |
This command writes sectors from a specified memory address to the SD card. When operation result is not `SD_OK`, then `ERR` packet is returned.
---
### `D`: **DD_SET_BLOCK_READY** ### `D`: **DD_SET_BLOCK_READY**
**Notify flashcart about 64DD block readiness** **Notify flashcart about 64DD block readiness**

View File

@ -36,11 +36,11 @@ module memory_bram (
eeprom_selected = 1'b0; eeprom_selected = 1'b0;
dd_selected = 1'b0; dd_selected = 1'b0;
flashram_selected = 1'b0; flashram_selected = 1'b0;
if (mem_bus.address[25:24] == 2'b01 && mem_bus.address[23:14] == 10'd0) begin if (mem_bus.address[26:24] == 3'h5) begin
buffer_selected = mem_bus.address[13] == 1'b0; buffer_selected = (mem_bus.address[23:0] >= 24'h00_0000 && mem_bus.address[23:0] < 24'h00_2000);
eeprom_selected = mem_bus.address[13:11] == 3'b100; eeprom_selected = (mem_bus.address[23:0] >= 24'h00_2000 && mem_bus.address[23:0] < 24'h00_2800);
dd_selected = mem_bus.address[13:8] == 6'b101000; dd_selected = (mem_bus.address[23:0] >= 24'h00_2800 && mem_bus.address[23:0] < 24'h00_2C00);
flashram_selected = mem_bus.address[13:7] == 7'b1010010; flashram_selected = (mem_bus.address[23:0] >= 24'h00_2C00 && mem_bus.address[23:0] < 24'h00_2C80);
end end
end end
@ -112,26 +112,26 @@ module memory_bram (
end end
// DD memory // 64DD/MCU buffer memory
logic [15:0] dd_bram [0:127]; logic [15:0] dd_bram [0:511];
logic [15:0] dd_bram_rdata; logic [15:0] dd_bram_rdata;
always_ff @(posedge clk) begin always_ff @(posedge clk) begin
if (write && dd_selected) begin if (write && dd_selected) begin
dd_bram[mem_bus.address[7:1]] <= mem_bus.wdata; dd_bram[mem_bus.address[9:1]] <= mem_bus.wdata;
end end
if (n64_scb.dd_write) begin if (n64_scb.dd_write) begin
dd_bram[n64_scb.dd_address] <= n64_scb.dd_wdata; dd_bram[{2'b00, n64_scb.dd_address}] <= n64_scb.dd_wdata;
end end
end end
always_ff @(posedge clk) begin always_ff @(posedge clk) begin
dd_bram_rdata <= dd_bram[mem_bus.address[7:1]]; dd_bram_rdata <= dd_bram[mem_bus.address[9:1]];
end end
always_ff @(posedge clk) begin always_ff @(posedge clk) begin
n64_scb.dd_rdata <= dd_bram[n64_scb.dd_address]; n64_scb.dd_rdata <= dd_bram[{2'b00, n64_scb.dd_address}];
end end

View File

@ -30,7 +30,7 @@
/ f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */ / f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */
#define FF_USE_MKFS 0 #define FF_USE_MKFS 1
/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */ /* This option switches f_mkfs() function. (0:Disable or 1:Enable) */
@ -133,7 +133,7 @@
/ ff_memfree() exemplified in ffsystem.c, need to be added to the project. */ / ff_memfree() exemplified in ffsystem.c, need to be added to the project. */
#define FF_LFN_UNICODE 0 #define FF_LFN_UNICODE 2
/* This option switches the character encoding on the API when LFN is enabled. /* This option switches the character encoding on the API when LFN is enabled.
/ /
/ 0: ANSI/OEM in current CP (TCHAR = char) / 0: ANSI/OEM in current CP (TCHAR = char)
@ -145,7 +145,7 @@
/ When LFN is not enabled, this option has no effect. */ / When LFN is not enabled, this option has no effect. */
#define FF_LFN_BUF 255 #define FF_LFN_BUF 1023
#define FF_SFN_BUF 12 #define FF_SFN_BUF 12
/* This set of options defines size of file name members in the FILINFO structure /* This set of options defines size of file name members in the FILINFO structure
/ which is used to read out directory items. These values should be suffcient for / which is used to read out directory items. These values should be suffcient for

View File

@ -194,6 +194,7 @@ const char *sc64_error_description (sc64_error_t error) {
case SD_ERROR_ACMD41_IO: return "ACMD41 I/O"; case SD_ERROR_ACMD41_IO: return "ACMD41 I/O";
case SD_ERROR_ACMD41_OCR: return "ACMD41 OCR"; case SD_ERROR_ACMD41_OCR: return "ACMD41 OCR";
case SD_ERROR_ACMD41_TIMEOUT: return "ACMD41 timeout"; case SD_ERROR_ACMD41_TIMEOUT: return "ACMD41 timeout";
case SD_ERROR_LOCKED: return "SD card is locked by the PC side";
default: return "Unknown error (SD)"; default: return "Unknown error (SD)";
} }
} }

View File

@ -55,6 +55,7 @@ typedef enum {
SD_ERROR_ACMD41_IO = 27, SD_ERROR_ACMD41_IO = 27,
SD_ERROR_ACMD41_OCR = 28, SD_ERROR_ACMD41_OCR = 28,
SD_ERROR_ACMD41_TIMEOUT = 29, SD_ERROR_ACMD41_TIMEOUT = 29,
SD_ERROR_LOCKED = 30,
} sc64_sd_error_t; } sc64_sd_error_t;
typedef uint32_t sc64_error_t; typedef uint32_t sc64_error_t;

View File

@ -141,6 +141,10 @@ static struct process p;
static bool cfg_cmd_check (void) { static bool cfg_cmd_check (void) {
if (!writeback_pending() && !hw_gpio_get(GPIO_ID_N64_RESET)) {
sd_release_lock(SD_LOCK_N64);
}
uint32_t reg = fpga_reg_get(REG_CFG_CMD); uint32_t reg = fpga_reg_get(REG_CFG_CMD);
if (reg & CFG_CMD_AUX_PENDING) { if (reg & CFG_CMD_AUX_PENDING) {
@ -634,21 +638,25 @@ void cfg_process (void) {
p.data[0] = p.usb_output_ready ? 0 : (1 << 31); p.data[0] = p.usb_output_ready ? 0 : (1 << 31);
break; break;
case CMD_ID_SD_CARD_OP: case CMD_ID_SD_CARD_OP: {
sd_error_t error = SD_OK;
switch (p.data[1]) { switch (p.data[1]) {
case SD_CARD_OP_DEINIT: case SD_CARD_OP_DEINIT:
sd_card_deinit(); error = sd_get_lock(SD_LOCK_N64);
break; if (error == SD_OK) {
sd_card_deinit();
case SD_CARD_OP_INIT: { sd_release_lock(SD_LOCK_N64);
led_activity_on(); }
sd_error_t error = sd_card_init(); break;
led_activity_off();
if (error != SD_OK) { case SD_CARD_OP_INIT:
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); error = sd_try_lock(SD_LOCK_N64);
if (error == SD_OK) {
led_activity_on();
error = sd_card_init();
led_activity_off();
} }
break; break;
}
case SD_CARD_OP_GET_STATUS: case SD_CARD_OP_GET_STATUS:
p.data[1] = sd_card_get_status(); p.data[1] = sd_card_get_status();
@ -658,36 +666,44 @@ void cfg_process (void) {
if (cfg_translate_address(&p.data[0], SD_CARD_INFO_SIZE, (SDRAM | BRAM))) { if (cfg_translate_address(&p.data[0], SD_CARD_INFO_SIZE, (SDRAM | BRAM))) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS);
} }
sd_error_t error = sd_card_get_info(p.data[0]); error = sd_get_lock(SD_LOCK_N64);
if (error != SD_OK) { if (error == SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); error = sd_card_get_info(p.data[0]);
} }
break; break;
case SD_CARD_OP_BYTE_SWAP_ON: { case SD_CARD_OP_BYTE_SWAP_ON:
sd_error_t error = sd_set_byte_swap(true); error = sd_get_lock(SD_LOCK_N64);
if (error != SD_OK) { if (error == SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); error = sd_set_byte_swap(true);
} }
break; break;
}
case SD_CARD_OP_BYTE_SWAP_OFF: { case SD_CARD_OP_BYTE_SWAP_OFF:
sd_error_t error = sd_set_byte_swap(false); error = sd_get_lock(SD_LOCK_N64);
if (error != SD_OK) { if (error == SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); error = sd_set_byte_swap(false);
} }
break; break;
}
default: default:
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_OPERATION); error = SD_ERROR_INVALID_OPERATION;
break;
}
if (error != SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error);
} }
break; break;
}
case CMD_ID_SD_SECTOR_SET: case CMD_ID_SD_SECTOR_SET: {
sd_error_t error = sd_get_lock(SD_LOCK_N64);
if (error != SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error);
}
p.sd_card_sector = p.data[0]; p.sd_card_sector = p.data[0];
break; break;
}
case CMD_ID_SD_READ: { case CMD_ID_SD_READ: {
if (p.data[1] >= 0x800000) { if (p.data[1] >= 0x800000) {
@ -696,9 +712,12 @@ void cfg_process (void) {
if (cfg_translate_address(&p.data[0], (p.data[1] * SD_SECTOR_SIZE), (SDRAM | FLASH | BRAM))) { if (cfg_translate_address(&p.data[0], (p.data[1] * SD_SECTOR_SIZE), (SDRAM | FLASH | BRAM))) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS);
} }
led_activity_on(); sd_error_t error = sd_get_lock(SD_LOCK_N64);
sd_error_t error = sd_read_sectors(p.data[0], p.sd_card_sector, p.data[1]); if (error == SD_OK) {
led_activity_off(); led_activity_on();
error = sd_read_sectors(p.data[0], p.sd_card_sector, p.data[1]);
led_activity_off();
}
if (error != SD_OK) { if (error != SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error);
} }
@ -713,9 +732,12 @@ void cfg_process (void) {
if (cfg_translate_address(&p.data[0], (p.data[1] * SD_SECTOR_SIZE), (SDRAM | FLASH | BRAM))) { if (cfg_translate_address(&p.data[0], (p.data[1] * SD_SECTOR_SIZE), (SDRAM | FLASH | BRAM))) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS);
} }
led_activity_on(); sd_error_t error = sd_get_lock(SD_LOCK_N64);
sd_error_t error = sd_write_sectors(p.data[0], p.sd_card_sector, p.data[1]); if (error == SD_OK) {
led_activity_off(); led_activity_on();
error = sd_write_sectors(p.data[0], p.sd_card_sector, p.data[1]);
led_activity_off();
}
if (error != SD_OK) { if (error != SD_OK) {
return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error);
} }

View File

@ -134,11 +134,14 @@ static bool dd_block_read_request (void) {
uint16_t index = dd_track_head_block(); uint16_t index = dd_track_head_block();
uint32_t buffer_address = DD_BLOCK_BUFFER_ADDRESS; uint32_t buffer_address = DD_BLOCK_BUFFER_ADDRESS;
if (p.sd_mode) { if (p.sd_mode) {
uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; sd_error_t error = sd_get_lock(SD_LOCK_N64);
uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, false); if (error == SD_OK) {
led_activity_on(); uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE];
sd_error_t error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_read_sectors); uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, false);
led_activity_off(); led_activity_on();
error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_read_sectors);
led_activity_off();
}
dd_set_block_ready(error == SD_OK); dd_set_block_ready(error == SD_OK);
} else { } else {
usb_tx_info_t packet_info; usb_tx_info_t packet_info;
@ -158,11 +161,14 @@ static bool dd_block_write_request (void) {
uint32_t index = dd_track_head_block(); uint32_t index = dd_track_head_block();
uint32_t buffer_address = DD_BLOCK_BUFFER_ADDRESS; uint32_t buffer_address = DD_BLOCK_BUFFER_ADDRESS;
if (p.sd_mode) { if (p.sd_mode) {
uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; sd_error_t error = sd_get_lock(SD_LOCK_N64);
uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, true); if (error == SD_OK) {
led_activity_on(); uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE];
sd_error_t error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_write_sectors); uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, true);
led_activity_off(); led_activity_on();
error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_write_sectors);
led_activity_off();
}
dd_set_block_ready(error == SD_OK); dd_set_block_ready(error == SD_OK);
} else { } else {
usb_tx_info_t packet_info; usb_tx_info_t packet_info;

View File

@ -6,7 +6,7 @@
#define FLASHRAM_SECTOR_SIZE (16 * 1024) #define FLASHRAM_SECTOR_SIZE (16 * 1024)
#define FLASHRAM_PAGE_SIZE (128) #define FLASHRAM_PAGE_SIZE (128)
#define FLASHRAM_ADDRESS (0x03FE0000UL) #define FLASHRAM_ADDRESS (0x03FE0000UL)
#define FLASHRAM_BUFFER_ADDRESS (0x05002900UL) #define FLASHRAM_BUFFER_ADDRESS (0x05002C00UL)
typedef enum { typedef enum {

View File

@ -4,7 +4,7 @@
#include "timer.h" #include "timer.h"
#define SD_INIT_BUFFER_ADDRESS (0x05002800UL) #define SD_INIT_BUFFER_ADDRESS (0x05002BB8UL)
#define BYTE_SWAP_ADDRESS_END (0x05000000UL) #define BYTE_SWAP_ADDRESS_END (0x05000000UL)
#define CMD6_ARG_CHECK_HS (0x00FFFFF1UL) #define CMD6_ARG_CHECK_HS (0x00FFFFF1UL)
@ -86,6 +86,7 @@ struct process {
uint8_t csd[16]; uint8_t csd[16];
uint8_t cid[16]; uint8_t cid[16];
bool byte_swap; bool byte_swap;
sd_lock_t lock;
}; };
@ -428,6 +429,7 @@ sd_error_t sd_card_init (void) {
void sd_card_deinit (void) { void sd_card_deinit (void) {
if (p.card_initialized) { if (p.card_initialized) {
p.card_initialized = false; p.card_initialized = false;
p.card_type_block = false;
p.byte_swap = false; p.byte_swap = false;
sd_set_clock(CLOCK_400KHZ); sd_set_clock(CLOCK_400KHZ);
sd_cmd(0, 0, RSP_NONE, NULL); sd_cmd(0, 0, RSP_NONE, NULL);
@ -575,10 +577,32 @@ sd_error_t sd_optimize_sectors (uint32_t address, uint32_t *sector_table, uint32
return SD_OK; return SD_OK;
} }
sd_error_t sd_get_lock (sd_lock_t lock) {
if (p.lock == lock) {
return SD_OK;
}
return SD_ERROR_LOCKED;
}
sd_error_t sd_try_lock (sd_lock_t lock) {
if (p.lock == SD_LOCK_NONE) {
p.lock = lock;
return SD_OK;
}
return sd_get_lock(lock);
}
void sd_release_lock (sd_lock_t lock) {
if (p.lock == lock) {
p.lock = SD_LOCK_NONE;
}
}
void sd_init (void) { void sd_init (void) {
p.card_initialized = false; p.card_initialized = false;
p.byte_swap = false; p.byte_swap = false;
p.lock = SD_LOCK_NONE;
sd_set_clock(CLOCK_STOP); sd_set_clock(CLOCK_STOP);
} }

View File

@ -41,10 +41,17 @@ typedef enum {
SD_ERROR_ACMD41_IO = 27, SD_ERROR_ACMD41_IO = 27,
SD_ERROR_ACMD41_OCR = 28, SD_ERROR_ACMD41_OCR = 28,
SD_ERROR_ACMD41_TIMEOUT = 29, SD_ERROR_ACMD41_TIMEOUT = 29,
SD_ERROR_LOCKED = 30,
} sd_error_t; } sd_error_t;
typedef sd_error_t sd_process_sectors_t (uint32_t address, uint32_t sector, uint32_t count); typedef sd_error_t sd_process_sectors_t (uint32_t address, uint32_t sector, uint32_t count);
typedef enum {
SD_LOCK_NONE,
SD_LOCK_N64,
SD_LOCK_USB,
} sd_lock_t;
sd_error_t sd_card_init (void); sd_error_t sd_card_init (void);
void sd_card_deinit (void); void sd_card_deinit (void);
@ -58,6 +65,10 @@ sd_error_t sd_read_sectors (uint32_t address, uint32_t sector, uint32_t count);
sd_error_t sd_optimize_sectors (uint32_t address, uint32_t *sector_table, uint32_t count, sd_process_sectors_t sd_process_sectors); sd_error_t sd_optimize_sectors (uint32_t address, uint32_t *sector_table, uint32_t count, sd_process_sectors_t sd_process_sectors);
sd_error_t sd_get_lock (sd_lock_t lock);
sd_error_t sd_try_lock (sd_lock_t lock);
void sd_release_lock (sd_lock_t lock);
void sd_init (void); void sd_init (void);
void sd_process (void); void sd_process (void);

View File

@ -4,7 +4,9 @@
#include "flash.h" #include "flash.h"
#include "fpga.h" #include "fpga.h"
#include "hw.h" #include "hw.h"
#include "led.h"
#include "rtc.h" #include "rtc.h"
#include "sd.h"
#include "timer.h" #include "timer.h"
#include "update.h" #include "update.h"
#include "usb.h" #include "usb.h"
@ -15,7 +17,7 @@
#define BOOTLOADER_ADDRESS (0x04E00000UL) #define BOOTLOADER_ADDRESS (0x04E00000UL)
#define BOOTLOADER_LENGTH (1920 * 1024) #define BOOTLOADER_LENGTH (1920 * 1024)
#define MEMORY_LENGTH (0x05002980UL) #define MEMORY_LENGTH (0x05002C80UL)
#define RX_FLUSH_ADDRESS (0x07F00000UL) #define RX_FLUSH_ADDRESS (0x07F00000UL)
#define RX_FLUSH_LENGTH (1 * 1024 * 1024) #define RX_FLUSH_LENGTH (1 * 1024 * 1024)
@ -268,6 +270,7 @@ static void usb_rx_process (void) {
case 'R': case 'R':
cfg_reset_state(); cfg_reset_state();
cic_reset_parameters(); cic_reset_parameters();
sd_release_lock(SD_LOCK_USB);
p.rx_state = RX_STATE_IDLE; p.rx_state = RX_STATE_IDLE;
p.response_pending = true; p.response_pending = true;
break; break;
@ -384,6 +387,119 @@ static void usb_rx_process (void) {
p.response_pending = true; p.response_pending = true;
break; break;
case 'i': {
sd_error_t error = SD_OK;
switch (p.rx_args[1]) {
case 0:
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
sd_card_deinit();
sd_release_lock(SD_LOCK_USB);
}
break;
case 1:
error = sd_try_lock(SD_LOCK_USB);
if (error == SD_OK) {
led_activity_on();
error = sd_card_init();
led_activity_off();
}
break;
case 2:
break;
case 3:
if (usb_validate_address_length(p.rx_args[0], SD_CARD_INFO_SIZE, true)) {
error = SD_ERROR_INVALID_ADDRESS;
} else {
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
error = sd_card_get_info(p.rx_args[0]);
}
}
break;
case 4:
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
error = sd_set_byte_swap(true);
}
break;
case 5:
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
error = sd_set_byte_swap(false);
}
break;
default:
error = SD_ERROR_INVALID_OPERATION;
break;
}
p.rx_state = RX_STATE_IDLE;
p.response_pending = true;
p.response_error = (error != SD_OK);
p.response_info.data_length = 8;
p.response_info.data[0] = error;
p.response_info.data[1] = sd_card_get_status();
break;
}
case 's': {
uint32_t sector = 0;
if (!usb_rx_word(&sector)) {
break;
}
sd_error_t error = SD_OK;
if (p.rx_args[1] >= 0x800000) {
error = SD_ERROR_INVALID_ARGUMENT;
} else if (usb_validate_address_length(p.rx_args[0], (p.rx_args[1] * SD_SECTOR_SIZE), true)) {
error = SD_ERROR_INVALID_ADDRESS;
} else {
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
led_activity_on();
error = sd_read_sectors(p.rx_args[0], sector, p.rx_args[1]);
led_activity_off();
}
}
p.rx_state = RX_STATE_IDLE;
p.response_pending = true;
p.response_error = (error != SD_OK);
p.response_info.data_length = 4;
p.response_info.data[0] = error;
break;
}
case 'S': {
uint32_t sector = 0;
if (!usb_rx_word(&sector)) {
break;
}
sd_error_t error = SD_OK;
if (p.rx_args[1] >= 0x800000) {
error = SD_ERROR_INVALID_ARGUMENT;
} else if (usb_validate_address_length(p.rx_args[0], (p.rx_args[1] * SD_SECTOR_SIZE), true)) {
error = SD_ERROR_INVALID_ADDRESS;
} else {
error = sd_get_lock(SD_LOCK_USB);
if (error == SD_OK) {
led_activity_on();
error = sd_write_sectors(p.rx_args[0], sector, p.rx_args[1]);
led_activity_off();
}
}
p.rx_state = RX_STATE_IDLE;
p.response_pending = true;
p.response_error = (error != SD_OK);
p.response_info.data_length = 4;
p.response_info.data[0] = error;
break;
}
case 'D': case 'D':
dd_set_block_ready(p.rx_args[0] == 0); dd_set_block_ready(p.rx_args[0] == 0);
p.rx_state = RX_STATE_IDLE; p.rx_state = RX_STATE_IDLE;
@ -635,5 +751,6 @@ void usb_process (void) {
usb_tx_process(); usb_tx_process();
} else { } else {
usb_flush_packet(); usb_flush_packet();
sd_release_lock(SD_LOCK_USB);
} }
} }

View File

@ -72,19 +72,20 @@ static save_type_t writeback_get_address_length (uint32_t *address, uint32_t *le
} }
static void writeback_save_to_sd (void) { static void writeback_save_to_sd (void) {
save_type_t save;
uint32_t address; uint32_t address;
uint32_t length; uint32_t length;
save = writeback_get_address_length(&address, &length); if (writeback_get_address_length(&address, &length) == SAVE_TYPE_NONE) {
if (save == SAVE_TYPE_NONE) {
writeback_disable(); writeback_disable();
return; return;
} }
sd_error_t error = sd_optimize_sectors(address, p.sectors, (length / SD_SECTOR_SIZE), sd_write_sectors); if (sd_get_lock(SD_LOCK_N64) != SD_OK) {
writeback_disable();
return;
}
if (error != SD_OK) { if (sd_optimize_sectors(address, p.sectors, (length / SD_SECTOR_SIZE), sd_write_sectors) != SD_OK) {
writeback_disable(); writeback_disable();
return; return;
} }

124
sw/deployer/Cargo.lock generated
View File

@ -124,7 +124,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -162,6 +162,26 @@ dependencies = [
"arrayvec", "arrayvec",
] ]
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.77",
]
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -218,13 +238,22 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.103" version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2755ff20a1d93490d26ba33a6f092a38a508398a5320df5d4b3014fcccce9410" checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
"once_cell", "shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
] ]
[[package]] [[package]]
@ -263,6 +292,17 @@ dependencies = [
"windows-targets 0.52.5", "windows-targets 0.52.5",
] ]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.8" version = "4.5.8"
@ -303,7 +343,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -482,6 +522,12 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "half" name = "half"
version = "2.4.1" version = "2.4.1"
@ -633,7 +679,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -755,6 +801,16 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "libudev" name = "libudev"
version = "0.3.0" version = "0.3.0"
@ -922,7 +978,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1007,6 +1063,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettyplease"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn 2.0.77",
]
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.20+deprecated" version = "0.5.20+deprecated"
@ -1038,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1219,10 +1285,18 @@ dependencies = [
"ordered-multimap", "ordered-multimap",
] ]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "sc64deployer" name = "sc64deployer"
version = "2.19.0" version = "2.19.0"
dependencies = [ dependencies = [
"bindgen",
"cc",
"chrono", "chrono",
"clap", "clap",
"clap-num", "clap-num",
@ -1266,14 +1340,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.6" version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1308,6 +1382,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@ -1357,9 +1437,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.68" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1402,7 +1482,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1430,18 +1510,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.6" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.14" version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -1527,7 +1607,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -1549,7 +1629,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.77",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -1738,9 +1818,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.13" version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

@ -6,6 +6,10 @@ authors = ["Polprzewodnikowy"]
description = "SummerCart64 loader and control software" description = "SummerCart64 loader and control software"
documentation = "https://github.com/Polprzewodnikowy/SummerCart64" documentation = "https://github.com/Polprzewodnikowy/SummerCart64"
[build-dependencies]
bindgen = "0.70.1"
cc = "1.1.18"
[dependencies] [dependencies]
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.8", features = ["derive"] } clap = { version = "4.5.8", features = ["derive"] }

19
sw/deployer/build.rs Normal file
View File

@ -0,0 +1,19 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
cc::Build::new()
.file("../bootloader/src/fatfs/ff.c")
.file("../bootloader/src/fatfs/ffsystem.c")
.file("../bootloader/src/fatfs/ffunicode.c")
.compile("fatfs");
bindgen::Builder::default()
.header("../bootloader/src/fatfs/ff.h")
.blocklist_function("get_fattime")
.generate()
.expect("Unable to generate FatFs bindings")
.write_to_file(out_dir.join("fatfs_bindings.rs"))
.expect("Unable to write FatFs bindings");
Ok(())
}

View File

@ -11,12 +11,13 @@ use panic_message::panic_message;
use std::{ use std::{
fs::File, fs::File,
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
panic,
path::PathBuf, path::PathBuf,
process,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc,
}, },
{panic, process},
}; };
#[derive(Parser)] #[derive(Parser)]
@ -57,6 +58,12 @@ enum Commands {
/// Dump data from arbitrary location in SC64 memory space /// Dump data from arbitrary location in SC64 memory space
Dump(DumpArgs), Dump(DumpArgs),
/// Perform operations on the SD card
SD {
#[command(subcommand)]
command: SDCommands,
},
/// Print information about connected SC64 device /// Print information about connected SC64 device
Info, Info,
@ -202,6 +209,71 @@ struct DumpArgs {
path: PathBuf, path: PathBuf,
} }
#[derive(Subcommand)]
enum SDCommands {
/// List a directory on the SD card
#[command(name = "ls")]
List {
/// Path to the directory
path: Option<PathBuf>,
},
/// Display a file or directory status
#[command(name = "stat")]
Stat {
/// Path to the file or directory
path: PathBuf,
},
/// Move or rename a file or directory
#[command(name = "mv")]
Move {
/// Path to the current file or directory
src: PathBuf,
/// Path to the new file or directory
dst: PathBuf,
},
/// Remove a file or empty directory
#[command(name = "rm")]
Delete {
/// Path to the file or directory
path: PathBuf,
},
/// Create a new directory
#[command(name = "mkdir")]
CreateDirectory {
/// Path to the directory
path: PathBuf,
},
/// Download a file to the PC
#[command(name = "download")]
Download {
/// Path to the file on the SD card
src: PathBuf,
/// Path to the file on the PC
dst: Option<PathBuf>,
},
/// Upload a file to the SD card
#[command(name = "upload")]
Upload {
/// Path to the file on the PC
src: PathBuf,
/// Path to the file on the SD card
dst: Option<PathBuf>,
},
/// Format the SD card
#[command(name = "mkfs")]
Format,
}
#[derive(Subcommand)] #[derive(Subcommand)]
enum SetCommands { enum SetCommands {
/// Synchronize real time clock (RTC) on the SC64 with local system time /// Synchronize real time clock (RTC) on the SC64 with local system time
@ -318,6 +390,9 @@ fn main() {
panic::set_hook(Box::new(|_| {})); panic::set_hook(Box::new(|_| {}));
} }
#[cfg(windows)]
colored::control::set_virtual_terminal(true).ok();
match panic::catch_unwind(|| handle_command(&cli.command, cli.port, cli.remote)) { match panic::catch_unwind(|| handle_command(&cli.command, cli.port, cli.remote)) {
Ok(_) => {} Ok(_) => {}
Err(payload) => { Err(payload) => {
@ -340,6 +415,7 @@ fn handle_command(command: &Commands, port: Option<String>, remote: Option<Strin
Commands::_64DD(args) => handle_64dd_command(connection, args), Commands::_64DD(args) => handle_64dd_command(connection, args),
Commands::Debug(args) => handle_debug_command(connection, args), Commands::Debug(args) => handle_debug_command(connection, args),
Commands::Dump(args) => handle_dump_command(connection, args), Commands::Dump(args) => handle_dump_command(connection, args),
Commands::SD { command } => handle_sd_command(connection, command),
Commands::Info => handle_info_command(connection), Commands::Info => handle_info_command(connection),
Commands::Reset => handle_reset_command(connection), Commands::Reset => handle_reset_command(connection),
Commands::Set { command } => handle_set_command(connection, command), Commands::Set { command } => handle_set_command(connection, command),
@ -767,16 +843,167 @@ fn handle_dump_command(connection: Connection, args: &DumpArgs) -> Result<(), sc
Ok(()) Ok(())
} }
fn handle_sd_command(connection: Connection, command: &SDCommands) -> Result<(), sc64::Error> {
let mut sc64 = init_sc64(connection, true)?;
match sc64.init_sd_card()? {
sc64::SdCardResult::OK => {}
error => {
return Err(sc64::Error::new(
format!("Couldn't init the SD card: {error}").as_str(),
))
}
}
if sc64.is_console_powered_on()? {
println!(
"{}\n{}\n{}",
"========== [WARNING] ==========".bold().bright_yellow(),
"The console is powered on. To avoid potential data corruption it's strongly"
.bright_yellow(),
"recommended to access the SD card only when the console is powered off."
.bright_yellow()
);
let answer = prompt(format!("{}", "Continue anyways? [y/N] ".bold()));
if answer.to_ascii_lowercase() != "y" {
sc64.deinit_sd_card()?;
println!("{}", "SD card access aborted".red());
return Ok(());
}
}
sc64.reset_state()?;
let mut ff = sc64::ff::FatFs::new(sc64)?;
match command {
SDCommands::List { path } => {
for item in ff.list(path.clone().unwrap_or(PathBuf::from("/")))? {
let sc64::ff::Entry {
info,
datetime,
name,
} = item;
let name = match info {
sc64::ff::EntryInfo::Directory => ("/".to_owned() + &name).bright_blue(),
sc64::ff::EntryInfo::File { size: _ } => name.bright_green(),
};
println!("{info} {datetime} | {}", name.bold());
}
}
SDCommands::Stat { path } => {
let sc64::ff::Entry {
info,
datetime,
name,
} = ff.stat(path)?;
let name = match info {
sc64::ff::EntryInfo::Directory => ("/".to_owned() + &name).bright_blue(),
sc64::ff::EntryInfo::File { size: _ } => name.bright_green(),
};
println!("{info} {datetime} | {}", name.bold());
}
SDCommands::Move { src, dst } => {
ff.rename(src, dst)?;
println!(
"Successfully moved {} to {}",
src.to_str().unwrap_or_default().bright_green(),
dst.to_str().unwrap_or_default().bright_green()
);
}
SDCommands::Delete { path } => {
ff.delete(path)?;
println!(
"Successfully deleted {}",
path.to_str().unwrap_or_default().bright_green()
);
}
SDCommands::CreateDirectory { path } => {
ff.mkdir(path)?;
println!(
"Successfully created {}",
path.to_str().unwrap_or_default().bright_green()
);
}
SDCommands::Download { src, dst } => {
let dst = &dst.clone().unwrap_or(
src.file_name()
.map(PathBuf::from)
.ok_or(sc64::ff::Error::InvalidParameter)?,
);
let mut src_file = ff.open(src)?;
let mut dst_file = std::fs::File::create(dst)?;
let mut buffer = vec![0; 128 * 1024];
log_wait(
format!(
"Downloading {} to {}",
src.to_str().unwrap_or_default().bright_green(),
dst.to_str().unwrap_or_default().bright_green()
),
|| loop {
match src_file.read(&mut buffer)? {
0 => return Ok(()),
bytes => {
if let Err(e) = dst_file.write_all(&buffer[0..bytes]) {
return Err(e);
}
}
}
},
)?;
}
SDCommands::Upload { src, dst } => {
let dst = &dst.clone().unwrap_or(
src.file_name()
.map(PathBuf::from)
.ok_or(sc64::ff::Error::InvalidParameter)?,
);
let mut src_file = std::fs::File::open(src)?;
let mut dst_file = ff.create(dst)?;
let mut buffer = vec![0; 128 * 1024];
log_wait(
format!(
"Uploading {} to {}",
src.to_str().unwrap_or_default().bright_green(),
dst.to_str().unwrap_or_default().bright_green()
),
|| loop {
match src_file.read(&mut buffer)? {
0 => return Ok(()),
bytes => {
if let Err(e) = dst_file.write_all(&buffer[0..bytes]) {
return Err(e);
}
}
}
},
)?;
}
SDCommands::Format => {
let answer = prompt(format!(
"{}",
"Do you really want to format the SD card? [y/N] ".bold()
));
if answer.to_ascii_lowercase() != "y" {
println!("{}", "Format operation aborted".red());
return Ok(());
}
log_wait(format!("Formatting the SD card"), || ff.mkfs())?;
}
}
Ok(())
}
fn handle_info_command(connection: Connection) -> Result<(), sc64::Error> { fn handle_info_command(connection: Connection) -> Result<(), sc64::Error> {
let mut sc64 = init_sc64(connection, true)?; let mut sc64 = init_sc64(connection, true)?;
let (major, minor, revision) = sc64.check_firmware_version()?; let (major, minor, revision) = sc64.check_firmware_version()?;
let state = sc64.get_device_state()?; let state = sc64.get_device_state()?;
let datetime = state.datetime.format("%Y-%m-%d %H:%M:%S");
println!("{}", "SummerCart64 state information:".bold()); println!("{}", "SummerCart64 state information:".bold());
println!(" Firmware version: v{}.{}.{}", major, minor, revision); println!(" Firmware version: v{}.{}.{}", major, minor, revision);
println!(" RTC datetime: {}", datetime); println!(" RTC datetime: {}", state.datetime);
println!(" Boot mode: {}", state.boot_mode); println!(" Boot mode: {}", state.boot_mode);
println!(" Save type: {}", state.save_type); println!(" Save type: {}", state.save_type);
println!(" CIC seed: {}", state.cic_seed); println!(" CIC seed: {}", state.cic_seed);
@ -793,6 +1020,7 @@ fn handle_info_command(connection: Connection) -> Result<(), sc64::Error> {
println!(" Button state: {}", state.button_state); println!(" Button state: {}", state.button_state);
println!(" LED blink: {}", state.led_enable); println!(" LED blink: {}", state.led_enable);
println!(" IS-Viewer 64: {}", state.isviewer); println!(" IS-Viewer 64: {}", state.isviewer);
println!(" SD card status: {}", state.sd_card_status);
println!("{}", "SummerCart64 diagnostic information:".bold()); println!("{}", "SummerCart64 diagnostic information:".bold());
println!(" PI I/O access: {}", state.fpga_debug_data.pi_io_access); println!(" PI I/O access: {}", state.fpga_debug_data.pi_io_access);
println!( println!(
@ -921,19 +1149,30 @@ fn handle_firmware_command(
} }
fn handle_test_command(connection: Connection) -> Result<(), sc64::Error> { fn handle_test_command(connection: Connection) -> Result<(), sc64::Error> {
let mut sc64 = init_sc64(connection, false)?; let mut sc64 = init_sc64(connection, true)?;
sc64.reset_state()?;
println!("{}: USB", "[SC64 Tests]".bold()); println!("{}: USB", "[SC64 Tests]".bold());
print!(" Performing USB read speed test... "); print!(" Performing USB read speed test... ");
stdout().flush().unwrap(); stdout().flush().unwrap();
let read_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Read)?; let usb_read_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Read)?;
println!("{}", format!("{read_speed:.2} MiB/s",).bright_green()); println!("{}", format!("{usb_read_speed:.2} MiB/s",).bright_green());
print!(" Performing USB write speed test... "); print!(" Performing USB write speed test... ");
stdout().flush().unwrap(); stdout().flush().unwrap();
let write_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Write)?; let usb_write_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Write)?;
println!("{}", format!("{write_speed:.2} MiB/s",).bright_green()); println!("{}", format!("{usb_write_speed:.2} MiB/s",).bright_green());
println!("{}: SD card", "[SC64 Tests]".bold());
print!(" Performing SD card read speed test... ");
stdout().flush().unwrap();
match sc64.test_sd_card() {
Ok(sd_read_speed) => println!("{}", format!("{sd_read_speed:.2} MiB/s",).bright_green()),
Err(result) => println!("{}", format!("error! {result}").bright_red()),
}
println!("{}: SDRAM (pattern)", "[SC64 Tests]".bold()); println!("{}: SDRAM (pattern)", "[SC64 Tests]".bold());

View File

@ -26,3 +26,9 @@ impl From<std::io::Error> for Error {
Error::new(format!("IO error: {}", value).as_str()) Error::new(format!("IO error: {}", value).as_str())
} }
} }
impl From<super::ff::Error> for Error {
fn from(value: super::ff::Error) -> Self {
Error::new(format!("FatFs error: {}", value).as_str())
}
}

631
sw/deployer/src/sc64/ff.rs Normal file
View File

@ -0,0 +1,631 @@
use super::{SdCardResult, SC64, SD_CARD_SECTOR_SIZE};
use chrono::{Datelike, Timelike};
mod fatfs {
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(unused)]
include!(concat!(env!("OUT_DIR"), "/fatfs_bindings.rs"));
pub type DSTATUS = BYTE;
pub const DSTATUS_STA_OK: DSTATUS = 0;
pub const DSTATUS_STA_NOINIT: DSTATUS = 1 << 0;
pub const DSTATUS_STA_NODISK: DSTATUS = 1 << 2;
pub const DSTATUS_STA_PROTECT: DSTATUS = 1 << 3;
pub type DRESULT = std::os::raw::c_uint;
pub const DRESULT_RES_OK: DRESULT = 0;
pub const DRESULT_RES_ERROR: DRESULT = 1;
pub const DRESULT_RES_WRPRT: DRESULT = 2;
pub const DRESULT_RES_NOTRDY: DRESULT = 3;
pub const DRESULT_RES_PARERR: DRESULT = 4;
pub const CTRL_SYNC: BYTE = 0;
pub const GET_SECTOR_COUNT: BYTE = 1;
pub const GET_SECTOR_SIZE: BYTE = 2;
pub const GET_BLOCK_SIZE: BYTE = 3;
pub const CTRL_TRIM: BYTE = 4;
pub enum Error {
DiskErr,
IntErr,
NotReady,
NoFile,
NoPath,
InvalidName,
Denied,
Exist,
InvalidObject,
WriteProtected,
InvalidDrive,
NotEnabled,
NoFilesystem,
MkfsAborted,
Timeout,
Locked,
NotEnoughCore,
TooManyOpenFiles,
InvalidParameter,
DriverInstalled,
DriverNotInstalled,
Unknown,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::DiskErr => "A hard error occurred in the low level disk I/O layer",
Self::IntErr => "Assertion failed",
Self::NotReady => "The physical drive cannot work",
Self::NoFile => "Could not find the file",
Self::NoPath => "Could not find the path",
Self::InvalidName => "The path name format is invalid",
Self::Denied => "Access denied due to prohibited access or directory full",
Self::Exist => "Access denied due to prohibited access",
Self::InvalidObject => "The file/directory object is invalid",
Self::WriteProtected => "The physical drive is write protected",
Self::InvalidDrive => "The logical drive number is invalid",
Self::NotEnabled => "The volume has no work area",
Self::NoFilesystem => "There is no valid FAT volume",
Self::MkfsAborted => "The f_mkfs() aborted due to any problem",
Self::Timeout => "Could not get a grant to access the volume within defined period",
Self::Locked => "The operation is rejected according to the file sharing policy",
Self::NotEnoughCore => "LFN working buffer could not be allocated",
Self::TooManyOpenFiles => "Number of open files > FF_FS_LOCK",
Self::InvalidParameter => "Given parameter is invalid",
Self::DriverInstalled => "FatFs driver is already installed",
Self::DriverNotInstalled => "FatFs driver is not installed",
Self::Unknown => "Unknown error",
})
}
}
impl From<FRESULT> for Error {
fn from(value: FRESULT) -> Self {
match value {
FRESULT_FR_DISK_ERR => Error::DiskErr,
FRESULT_FR_INT_ERR => Error::IntErr,
FRESULT_FR_NOT_READY => Error::NotReady,
FRESULT_FR_NO_FILE => Error::NoFile,
FRESULT_FR_NO_PATH => Error::NoPath,
FRESULT_FR_INVALID_NAME => Error::InvalidName,
FRESULT_FR_DENIED => Error::Denied,
FRESULT_FR_EXIST => Error::Exist,
FRESULT_FR_INVALID_OBJECT => Error::InvalidObject,
FRESULT_FR_WRITE_PROTECTED => Error::WriteProtected,
FRESULT_FR_INVALID_DRIVE => Error::InvalidDrive,
FRESULT_FR_NOT_ENABLED => Error::NotEnabled,
FRESULT_FR_NO_FILESYSTEM => Error::NoFilesystem,
FRESULT_FR_MKFS_ABORTED => Error::MkfsAborted,
FRESULT_FR_TIMEOUT => Error::Timeout,
FRESULT_FR_LOCKED => Error::Locked,
FRESULT_FR_NOT_ENOUGH_CORE => Error::NotEnoughCore,
FRESULT_FR_TOO_MANY_OPEN_FILES => Error::TooManyOpenFiles,
FRESULT_FR_INVALID_PARAMETER => Error::InvalidParameter,
_ => Error::Unknown,
}
}
}
pub fn path<P: AsRef<std::path::Path>>(path: P) -> Result<std::ffi::CString, Error> {
match path.as_ref().to_str() {
Some(path) => Ok(std::ffi::CString::new(path).map_err(|_| Error::InvalidParameter)?),
None => Err(Error::InvalidParameter),
}
}
}
pub type Error = fatfs::Error;
static mut DRIVER: std::sync::Mutex<Option<Box<dyn FFDriver>>> = std::sync::Mutex::new(None);
fn install_driver(driver: impl FFDriver + 'static) -> Result<(), Error> {
let mut d = unsafe { DRIVER.lock().unwrap() };
if d.is_some() {
return Err(Error::DriverInstalled);
}
d.replace(Box::new(driver));
Ok(())
}
fn uninstall_driver() -> Result<(), Error> {
let mut d = unsafe { DRIVER.lock().unwrap() };
if d.is_none() {
return Err(Error::DriverNotInstalled);
}
d.take().unwrap().deinit();
Ok(())
}
pub struct FatFs {
fs: Box<fatfs::FATFS>,
}
impl FatFs {
pub fn new(driver: impl FFDriver + 'static) -> Result<Self, Error> {
install_driver(driver)?;
let mut ff = Self {
fs: Box::new(unsafe { std::mem::zeroed() }),
};
ff.mount(false)?;
Ok(ff)
}
fn mount(&mut self, force: bool) -> Result<(), Error> {
let opt = if force { 1 } else { 0 };
match unsafe { fatfs::f_mount(&mut *self.fs, fatfs::path("")?.as_ptr(), opt) } {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
fn unmount(&mut self) -> Result<(), Error> {
match unsafe { fatfs::f_mount(std::ptr::null_mut(), fatfs::path("")?.as_ptr(), 0) } {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
pub fn open<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<File, Error> {
File::open(
path,
fatfs::FA_OPEN_EXISTING | fatfs::FA_READ | fatfs::FA_WRITE,
)
}
pub fn create<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<File, Error> {
File::open(
path,
fatfs::FA_CREATE_ALWAYS | fatfs::FA_READ | fatfs::FA_WRITE,
)
}
pub fn stat<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<Entry, Error> {
let mut fno = unsafe { std::mem::zeroed() };
match unsafe { fatfs::f_stat(fatfs::path(path)?.as_ptr(), &mut fno) } {
fatfs::FRESULT_FR_OK => Ok(fno.into()),
error => Err(error.into()),
}
}
pub fn opendir<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<Directory, Error> {
Directory::open(path)
}
pub fn list<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<Vec<Entry>, Error> {
let mut dir = self.opendir(path)?;
let mut list = vec![];
while let Some(entry) = dir.read()? {
list.push(entry);
}
list.sort_by_key(|k| (k.info, k.name.to_lowercase()));
Ok(list)
}
pub fn mkdir<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), Error> {
match unsafe { fatfs::f_mkdir(fatfs::path(path)?.as_ptr()) } {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
pub fn delete<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), Error> {
match unsafe { fatfs::f_unlink(fatfs::path(path)?.as_ptr()) } {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
pub fn rename<P: AsRef<std::path::Path>>(&mut self, old: P, new: P) -> Result<(), Error> {
match unsafe { fatfs::f_rename(fatfs::path(old)?.as_ptr(), fatfs::path(new)?.as_ptr()) } {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
pub fn mkfs(&mut self) -> Result<(), Error> {
let mut work = [0u8; 16 * 1024];
match unsafe {
fatfs::f_mkfs(
fatfs::path("")?.as_ptr(),
std::ptr::null(),
work.as_mut_ptr().cast(),
size_of_val(&work) as u32,
)
} {
fatfs::FRESULT_FR_OK => Ok(()),
error => Err(error.into()),
}
}
}
impl Drop for FatFs {
fn drop(&mut self) {
self.unmount().ok();
uninstall_driver().ok();
}
}
pub enum IOCtl {
Sync,
GetSectorCount(fatfs::LBA_t),
GetSectorSize(fatfs::WORD),
GetBlockSize(fatfs::DWORD),
Trim,
}
pub trait FFDriver {
fn init(&mut self) -> fatfs::DSTATUS;
fn deinit(&mut self);
fn status(&mut self) -> fatfs::DSTATUS;
fn read(&mut self, buffer: &mut [u8], sector: fatfs::LBA_t) -> fatfs::DRESULT;
fn write(&mut self, buffer: &[u8], sector: fatfs::LBA_t) -> fatfs::DRESULT;
fn ioctl(&mut self, ioctl: &mut IOCtl) -> fatfs::DRESULT;
}
impl FFDriver for SC64 {
fn init(&mut self) -> fatfs::DSTATUS {
if let Ok(SdCardResult::OK) = self.init_sd_card() {
return fatfs::DSTATUS_STA_OK;
}
fatfs::DSTATUS_STA_NOINIT
}
fn deinit(&mut self) {
self.deinit_sd_card().ok();
}
fn status(&mut self) -> fatfs::DSTATUS {
if let Ok(status) = self.get_sd_card_status() {
if status.card_initialized {
return fatfs::DSTATUS_STA_OK;
}
}
fatfs::DSTATUS_STA_NOINIT
}
fn read(&mut self, buffer: &mut [u8], sector: fatfs::LBA_t) -> fatfs::DRESULT {
if let Ok(SdCardResult::OK) = self.read_sd_card(buffer, sector) {
return fatfs::DRESULT_RES_OK;
}
fatfs::DRESULT_RES_ERROR
}
fn write(&mut self, buffer: &[u8], sector: fatfs::LBA_t) -> fatfs::DRESULT {
if let Ok(SdCardResult::OK) = self.write_sd_card(buffer, sector) {
return fatfs::DRESULT_RES_OK;
}
fatfs::DRESULT_RES_ERROR
}
fn ioctl(&mut self, ioctl: &mut IOCtl) -> fatfs::DRESULT {
match ioctl {
IOCtl::Sync => {}
IOCtl::GetSectorCount(_) => {
match self.get_sd_card_info() {
Ok(info) => *ioctl = IOCtl::GetSectorCount(info.sectors as fatfs::LBA_t),
Err(_) => return fatfs::DRESULT_RES_ERROR,
};
}
IOCtl::GetSectorSize(_) => {
*ioctl = IOCtl::GetSectorSize(SD_CARD_SECTOR_SIZE as fatfs::WORD)
}
IOCtl::GetBlockSize(_) => {
*ioctl = IOCtl::GetBlockSize(1);
}
IOCtl::Trim => {}
}
fatfs::DRESULT_RES_OK
}
}
#[no_mangle]
unsafe extern "C" fn disk_status(pdrv: fatfs::BYTE) -> fatfs::DSTATUS {
if pdrv != 0 {
return fatfs::DSTATUS_STA_NOINIT;
}
if let Some(d) = DRIVER.lock().unwrap().as_mut() {
return d.status();
}
fatfs::DSTATUS_STA_NOINIT
}
#[no_mangle]
unsafe extern "C" fn disk_initialize(pdrv: fatfs::BYTE) -> fatfs::DSTATUS {
if pdrv != 0 {
return fatfs::DSTATUS_STA_NOINIT;
}
if let Some(d) = DRIVER.lock().unwrap().as_mut() {
return d.init();
}
fatfs::DSTATUS_STA_NOINIT
}
#[no_mangle]
unsafe extern "C" fn disk_read(
pdrv: fatfs::BYTE,
buff: *mut fatfs::BYTE,
sector: fatfs::LBA_t,
count: fatfs::UINT,
) -> fatfs::DRESULT {
if pdrv != 0 {
return fatfs::DRESULT_RES_PARERR;
}
if let Some(d) = DRIVER.lock().unwrap().as_mut() {
return d.read(
&mut *std::ptr::slice_from_raw_parts_mut(buff, (count as usize) * SD_CARD_SECTOR_SIZE),
sector,
);
}
fatfs::DRESULT_RES_NOTRDY
}
#[no_mangle]
unsafe extern "C" fn disk_write(
pdrv: fatfs::BYTE,
buff: *mut fatfs::BYTE,
sector: fatfs::LBA_t,
count: fatfs::UINT,
) -> fatfs::DRESULT {
if pdrv != 0 {
return fatfs::DRESULT_RES_PARERR;
}
if let Some(d) = DRIVER.lock().unwrap().as_mut() {
return d.write(
&*std::ptr::slice_from_raw_parts(buff, (count as usize) * SD_CARD_SECTOR_SIZE),
sector,
);
}
fatfs::DRESULT_RES_NOTRDY
}
#[no_mangle]
unsafe extern "C" fn disk_ioctl(
pdrv: fatfs::BYTE,
cmd: fatfs::BYTE,
buff: *mut std::os::raw::c_void,
) -> fatfs::DRESULT {
if pdrv != 0 {
return fatfs::DRESULT_RES_PARERR;
}
let mut ioctl = match cmd {
fatfs::CTRL_SYNC => IOCtl::Sync,
fatfs::GET_SECTOR_COUNT => IOCtl::GetSectorCount(0),
fatfs::GET_SECTOR_SIZE => IOCtl::GetSectorSize(0),
fatfs::GET_BLOCK_SIZE => IOCtl::GetBlockSize(0),
fatfs::CTRL_TRIM => IOCtl::Trim,
_ => return fatfs::DRESULT_RES_PARERR,
};
if let Some(d) = DRIVER.lock().unwrap().as_mut() {
let result = d.ioctl(&mut ioctl);
if result == fatfs::DRESULT_RES_OK {
match ioctl {
IOCtl::GetSectorCount(count) => {
buff.copy_from(std::ptr::addr_of!(count).cast(), size_of_val(&count))
}
IOCtl::GetSectorSize(size) => {
buff.copy_from(std::ptr::addr_of!(size).cast(), size_of_val(&size))
}
IOCtl::GetBlockSize(size) => {
buff.copy_from(std::ptr::addr_of!(size).cast(), size_of_val(&size))
}
_ => {}
}
}
return result;
}
fatfs::DRESULT_RES_NOTRDY
}
#[no_mangle]
unsafe extern "C" fn get_fattime() -> fatfs::DWORD {
let now = chrono::Local::now();
let year = now.year() as u32;
let month = now.month();
let day = now.day();
let hour = now.hour();
let minute = now.minute();
let second = now.second();
((year - 1980) << 25)
| (month << 21)
| (day << 16)
| (hour << 11)
| (minute << 5)
| (second << 1)
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Entry {
pub info: EntryInfo,
pub name: String,
pub datetime: chrono::NaiveDateTime,
}
#[derive(Clone, Copy, PartialEq, Eq, Ord)]
pub enum EntryInfo {
Directory,
File { size: u64 },
}
impl PartialOrd for EntryInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(EntryInfo::Directory, EntryInfo::File { size: _ }) => std::cmp::Ordering::Less,
(EntryInfo::File { size: _ }, EntryInfo::Directory) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
})
}
}
impl std::fmt::Display for EntryInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(match self {
EntryInfo::Directory => f.write_fmt(format_args!("d ----",))?,
EntryInfo::File { size } => {
const UNITS: [&str; 4] = ["K", "M", "G", "T"];
let mut reduced_size = *size as f64;
let mut reduced_unit = "B";
for unit in UNITS {
if reduced_size >= 1000.0 {
reduced_size /= 1024.0;
reduced_unit = unit;
} else {
break;
}
}
let formatted_size = if *size >= 1000 && reduced_size <= 9.9 {
format!("{:>1.1}{}", reduced_size, reduced_unit)
} else {
format!("{:>3.0}{}", reduced_size, reduced_unit)
};
f.write_fmt(format_args!("f {formatted_size}"))?
}
})
}
}
impl From<fatfs::FILINFO> for Entry {
fn from(value: fatfs::FILINFO) -> Self {
let name = unsafe { std::ffi::CStr::from_ptr(value.fname.as_ptr()) }
.to_string_lossy()
.to_string();
let datetime = chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(
(1980 + ((value.fdate >> 9) & 0x7F)).into(),
((value.fdate >> 5) & 0xF).into(),
(value.fdate & 0x1F).into(),
)
.unwrap_or_default(),
chrono::NaiveTime::from_hms_opt(
((value.ftime >> 11) & 0x1F).into(),
((value.ftime >> 5) & 0x3F).into(),
((value.ftime & 0x1F) * 2).into(),
)
.unwrap_or_default(),
);
let info = if (value.fattrib as u32 & fatfs::AM_DIR) == 0 {
EntryInfo::File { size: value.fsize }
} else {
EntryInfo::Directory
};
Self {
name,
datetime,
info,
}
}
}
pub struct Directory {
dir: fatfs::DIR,
}
impl Directory {
fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
let mut dir = unsafe { std::mem::zeroed() };
match unsafe { fatfs::f_opendir(&mut dir, fatfs::path(path)?.as_ptr()) } {
fatfs::FRESULT_FR_OK => Ok(Self { dir }),
error => Err(error.into()),
}
}
pub fn read(&mut self) -> Result<Option<Entry>, Error> {
let mut fno = unsafe { std::mem::zeroed() };
match unsafe { fatfs::f_readdir(&mut self.dir, &mut fno) } {
fatfs::FRESULT_FR_OK => {
if fno.fname[0] == 0 {
Ok(None)
} else {
Ok(Some(fno.into()))
}
}
error => Err(error.into()),
}
}
}
impl Drop for Directory {
fn drop(&mut self) {
unsafe { fatfs::f_closedir(&mut self.dir) };
}
}
pub struct File {
fil: fatfs::FIL,
}
impl File {
fn open<P: AsRef<std::path::Path>>(path: P, mode: u32) -> Result<File, Error> {
let mut fil = unsafe { std::mem::zeroed() };
match unsafe { fatfs::f_open(&mut fil, fatfs::path(path)?.as_ptr(), mode as u8) } {
fatfs::FRESULT_FR_OK => Ok(File { fil }),
error => Err(error.into()),
}
}
}
impl std::io::Read for File {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut bytes_read = 0;
match unsafe {
fatfs::f_read(
&mut self.fil,
buf.as_mut_ptr().cast(),
buf.len() as fatfs::UINT,
&mut bytes_read,
)
} {
fatfs::FRESULT_FR_OK => Ok(bytes_read as usize),
_ => Err(std::io::ErrorKind::BrokenPipe.into()),
}
}
}
impl std::io::Write for File {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut bytes_written = 0;
match unsafe {
fatfs::f_write(
&mut self.fil,
buf.as_ptr().cast(),
buf.len() as fatfs::UINT,
&mut bytes_written,
)
} {
fatfs::FRESULT_FR_OK => Ok(bytes_written as usize),
_ => Err(std::io::ErrorKind::BrokenPipe.into()),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match unsafe { fatfs::f_sync(&mut self.fil) } {
fatfs::FRESULT_FR_OK => Ok(()),
_ => Err(std::io::ErrorKind::BrokenPipe.into()),
}
}
}
impl std::io::Seek for File {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
let ofs = match pos {
std::io::SeekFrom::Current(offset) => self.fil.fptr.saturating_add_signed(offset),
std::io::SeekFrom::Start(offset) => offset,
std::io::SeekFrom::End(offset) => self.fil.obj.objsize.saturating_add_signed(offset),
};
match unsafe { fatfs::f_lseek(&mut self.fil, ofs) } {
fatfs::FRESULT_FR_OK => Ok(self.fil.fptr),
_ => Err(std::io::ErrorKind::BrokenPipe.into()),
}
}
}
impl Drop for File {
fn drop(&mut self) {
unsafe { fatfs::f_close(&mut self.fil) };
}
}

View File

@ -1,5 +1,6 @@
mod cic; mod cic;
mod error; mod error;
pub mod ff;
pub mod firmware; pub mod firmware;
mod ftdi; mod ftdi;
mod link; mod link;
@ -13,10 +14,11 @@ pub use self::{
link::list_local_devices, link::list_local_devices,
server::ServerEvent, server::ServerEvent,
types::{ types::{
AuxMessage, BootMode, ButtonMode, ButtonState, CicSeed, DataPacket, DdDiskState, AuxMessage, BootMode, ButtonMode, ButtonState, CicSeed, CicStep, DataPacket, DdDiskState,
DdDriveType, DdMode, DebugPacket, DiagnosticData, DiskPacket, DiskPacketKind, DdDriveType, DdMode, DebugPacket, DiagnosticData, DiskPacket, DiskPacketKind,
FpgaDebugData, ISViewer, MemoryTestPattern, MemoryTestPatternResult, SaveType, FpgaDebugData, ISViewer, MemoryTestPattern, MemoryTestPatternResult, SaveType,
SaveWriteback, SpeedTestDirection, Switch, TvType, SaveWriteback, SdCardInfo, SdCardOpPacket, SdCardResult, SdCardStatus, SpeedTestDirection,
Switch, TvType,
}, },
}; };
@ -25,7 +27,8 @@ use self::{
link::Link, link::Link,
time::{convert_from_datetime, convert_to_datetime}, time::{convert_from_datetime, convert_to_datetime},
types::{ types::{
get_config, get_setting, Config, ConfigId, FirmwareStatus, Setting, SettingId, UpdateStatus, get_config, get_setting, Config, ConfigId, FirmwareStatus, SdCardOp, Setting, SettingId,
UpdateStatus,
}, },
}; };
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -58,6 +61,7 @@ pub struct DeviceState {
pub button_mode: ButtonMode, pub button_mode: ButtonMode,
pub rom_extended_enable: Switch, pub rom_extended_enable: Switch,
pub led_enable: Switch, pub led_enable: Switch,
pub sd_card_status: SdCardStatus,
pub datetime: NaiveDateTime, pub datetime: NaiveDateTime,
pub fpga_debug_data: FpgaDebugData, pub fpga_debug_data: FpgaDebugData,
pub diagnostic_data: DiagnosticData, pub diagnostic_data: DiagnosticData,
@ -94,13 +98,18 @@ const SRAM_1M_LENGTH: usize = 128 * 1024;
const BOOTLOADER_ADDRESS: u32 = 0x04E0_0000; const BOOTLOADER_ADDRESS: u32 = 0x04E0_0000;
const SD_CARD_BUFFER_ADDRESS: u32 = 0x03FE_0000; // Arbitrary offset in SDRAM memory
const SD_CARD_BUFFER_LENGTH: usize = 128 * 1024; // Arbitrary length in SDRAM memory
pub const SD_CARD_SECTOR_SIZE: usize = 512;
const FIRMWARE_ADDRESS_SDRAM: u32 = 0x0010_0000; // Arbitrary offset in SDRAM memory const FIRMWARE_ADDRESS_SDRAM: u32 = 0x0010_0000; // Arbitrary offset in SDRAM memory
const FIRMWARE_ADDRESS_FLASH: u32 = 0x0410_0000; // Arbitrary offset in Flash memory const FIRMWARE_ADDRESS_FLASH: u32 = 0x0410_0000; // Arbitrary offset in Flash memory
const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90); const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90);
const ISV_BUFFER_LENGTH: usize = 64 * 1024; const ISV_BUFFER_LENGTH: usize = 64 * 1024;
pub const MEMORY_LENGTH: usize = 0x0500_2980; pub const MEMORY_LENGTH: usize = 0x0500_2C80;
const MEMORY_CHUNK_LENGTH: usize = 1 * 1024 * 1024; const MEMORY_CHUNK_LENGTH: usize = 1 * 1024 * 1024;
@ -235,6 +244,53 @@ impl SC64 {
Ok(()) Ok(())
} }
fn command_sd_card_operation(&mut self, op: SdCardOp) -> Result<SdCardOpPacket, Error> {
let data = self
.link
.execute_command_raw(b'i', op.into(), &[], false, true)?;
if data.len() != 8 {
return Err(Error::new(
"Invalid data length received for SD card operation command",
));
}
Ok(SdCardOpPacket {
result: data.clone().try_into()?,
status: data.clone().try_into()?,
})
}
fn command_sd_card_read(
&mut self,
address: u32,
sector: u32,
count: u32,
) -> Result<SdCardResult, Error> {
let data = self.link.execute_command_raw(
b's',
[address, count],
&sector.to_be_bytes(),
false,
true,
)?;
Ok(data.try_into()?)
}
fn command_sd_card_write(
&mut self,
address: u32,
sector: u32,
count: u32,
) -> Result<SdCardResult, Error> {
let data = self.link.execute_command_raw(
b'S',
[address, count],
&sector.to_be_bytes(),
false,
true,
)?;
Ok(data.try_into()?)
}
fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> { fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> {
self.link.execute_command(b'D', [error as u32, 0], &[])?; self.link.execute_command(b'D', [error as u32, 0], &[])?;
Ok(()) Ok(())
@ -489,6 +545,7 @@ impl SC64 {
button_mode: get_config!(self, ButtonMode)?, button_mode: get_config!(self, ButtonMode)?,
rom_extended_enable: get_config!(self, RomExtendedEnable)?, rom_extended_enable: get_config!(self, RomExtendedEnable)?,
led_enable: get_setting!(self, LedEnable)?, led_enable: get_setting!(self, LedEnable)?,
sd_card_status: self.get_sd_card_status()?,
datetime: self.get_datetime()?, datetime: self.get_datetime()?,
fpga_debug_data: self.command_fpga_debug_data_get()?, fpga_debug_data: self.command_fpga_debug_data_get()?,
diagnostic_data: self.command_diagnostic_data_get()?, diagnostic_data: self.command_diagnostic_data_get()?,
@ -606,6 +663,83 @@ impl SC64 {
Ok(false) Ok(false)
} }
pub fn init_sd_card(&mut self) -> Result<SdCardResult, Error> {
self.command_sd_card_operation(SdCardOp::Init)
.map(|packet| packet.result)
}
pub fn deinit_sd_card(&mut self) -> Result<SdCardResult, Error> {
self.command_sd_card_operation(SdCardOp::Deinit)
.map(|packet| packet.result)
}
pub fn get_sd_card_status(&mut self) -> Result<SdCardStatus, Error> {
self.command_sd_card_operation(SdCardOp::GetStatus)
.map(|reply| reply.status)
}
pub fn get_sd_card_info(&mut self) -> Result<SdCardInfo, Error> {
const SD_CARD_INFO_BUFFER_ADDRESS: u32 = 0x0500_2BE0;
let info =
match self.command_sd_card_operation(SdCardOp::GetInfo(SD_CARD_INFO_BUFFER_ADDRESS))? {
SdCardOpPacket {
result: SdCardResult::OK,
status: _,
} => self.command_memory_read(SD_CARD_INFO_BUFFER_ADDRESS, 32)?,
packet => {
return Err(Error::new(
format!("Couldn't get SD card info registers: {}", packet.result).as_str(),
))
}
};
Ok(info.try_into()?)
}
pub fn read_sd_card(&mut self, data: &mut [u8], sector: u32) -> Result<SdCardResult, Error> {
if data.len() % SD_CARD_SECTOR_SIZE != 0 {
return Err(Error::new(
"SD card read length not aligned to the sector size",
));
}
let mut current_sector = sector;
for mut chunk in data.chunks_mut(SD_CARD_BUFFER_LENGTH) {
let sectors = (chunk.len() / SD_CARD_SECTOR_SIZE) as u32;
match self.command_sd_card_read(SD_CARD_BUFFER_ADDRESS, current_sector, sectors)? {
SdCardResult::OK => {}
result => return Ok(result),
}
let data = self.command_memory_read(SD_CARD_BUFFER_ADDRESS, chunk.len())?;
chunk.write_all(&data)?;
current_sector += sectors;
}
Ok(SdCardResult::OK)
}
pub fn write_sd_card(&mut self, data: &[u8], sector: u32) -> Result<SdCardResult, Error> {
if data.len() % SD_CARD_SECTOR_SIZE != 0 {
return Err(Error::new(
"SD card write length not aligned to the sector size",
));
}
let mut current_sector = sector;
for chunk in data.chunks(SD_CARD_BUFFER_LENGTH) {
let sectors = (chunk.len() / SD_CARD_SECTOR_SIZE) as u32;
self.command_memory_write(SD_CARD_BUFFER_ADDRESS, chunk)?;
match self.command_sd_card_write(SD_CARD_BUFFER_ADDRESS, current_sector, sectors)? {
SdCardResult::OK => {}
result => return Ok(result),
}
current_sector += sectors;
}
Ok(SdCardResult::OK)
}
pub fn check_device(&mut self) -> Result<(), Error> { pub fn check_device(&mut self) -> Result<(), Error> {
let identifier = self.command_identifier_get().map_err(|e| { let identifier = self.command_identifier_get().map_err(|e| {
Error::new(format!("Couldn't get SC64 device identifier: {e}").as_str()) Error::new(format!("Couldn't get SC64 device identifier: {e}").as_str())
@ -634,6 +768,14 @@ impl SC64 {
self.command_state_reset() self.command_state_reset()
} }
pub fn is_console_powered_on(&mut self) -> Result<bool, Error> {
let debug_data = self.command_fpga_debug_data_get()?;
Ok(match debug_data.cic_step {
CicStep::Unavailable | CicStep::PowerOff => false,
_ => true,
})
}
pub fn backup_firmware(&mut self) -> Result<Vec<u8>, Error> { pub fn backup_firmware(&mut self) -> Result<Vec<u8>, Error> {
self.command_state_reset()?; self.command_state_reset()?;
let (status, length) = self.command_firmware_backup(FIRMWARE_ADDRESS_SDRAM)?; let (status, length) = self.command_firmware_backup(FIRMWARE_ADDRESS_SDRAM)?;
@ -738,6 +880,46 @@ impl SC64 {
Ok((TEST_LENGTH as f64 / MIB_DIVIDER) / elapsed.as_secs_f64()) Ok((TEST_LENGTH as f64 / MIB_DIVIDER) / elapsed.as_secs_f64())
} }
pub fn test_sd_card(&mut self) -> Result<f64, Error> {
const TEST_LENGTH: usize = 4 * 1024 * 1024;
const MIB_DIVIDER: f64 = 1024.0 * 1024.0;
let mut data = vec![0x00; TEST_LENGTH];
match self.init_sd_card()? {
SdCardResult::OK => {}
result => {
return Err(Error::new(
format!("Init SD card failed: {result}").as_str(),
))
}
}
let time = std::time::Instant::now();
match self.read_sd_card(&mut data, 0)? {
SdCardResult::OK => {}
result => {
return Err(Error::new(
format!("Read SD card failed: {result}").as_str(),
))
}
}
let elapsed = time.elapsed();
match self.deinit_sd_card()? {
SdCardResult::OK => {}
result => {
return Err(Error::new(
format!("Deinit SD card failed: {result}").as_str(),
))
}
}
Ok((TEST_LENGTH as f64 / MIB_DIVIDER) / elapsed.as_secs_f64())
}
pub fn test_sdram_pattern( pub fn test_sdram_pattern(
&mut self, &mut self,
pattern: MemoryTestPattern, pattern: MemoryTestPattern,

View File

@ -737,6 +737,241 @@ impl TryFrom<Vec<u8>> for DiskPacket {
} }
} }
pub enum SdCardOp {
Deinit,
Init,
GetStatus,
GetInfo(u32),
ByteSwapOn,
ByteSwapOff,
}
impl From<SdCardOp> for [u32; 2] {
fn from(value: SdCardOp) -> Self {
match value {
SdCardOp::Deinit => [0, 0],
SdCardOp::Init => [0, 1],
SdCardOp::GetStatus => [0, 2],
SdCardOp::GetInfo(address) => [address, 3],
SdCardOp::ByteSwapOn => [0, 4],
SdCardOp::ByteSwapOff => [0, 5],
}
}
}
pub enum SdCardResult {
OK,
NoCardInSlot,
NotInitialized,
InvalidArgument,
InvalidAddress,
InvalidOperation,
Cmd2IO,
Cmd3IO,
Cmd6CheckIO,
Cmd6CheckCRC,
Cmd6CheckTimeout,
Cmd6CheckResponse,
Cmd6SwitchIO,
Cmd6SwitchCRC,
Cmd6SwitchTimeout,
Cmd6SwitchResponse,
Cmd7IO,
Cmd8IO,
Cmd9IO,
Cmd10IO,
Cmd18IO,
Cmd18CRC,
Cmd18Timeout,
Cmd25IO,
Cmd25CRC,
Cmd25Timeout,
Acmd6IO,
Acmd41IO,
Acmd41OCR,
Acmd41Timeout,
Locked,
}
impl Display for SdCardResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::OK => "OK",
Self::NoCardInSlot => "No card in slot",
Self::NotInitialized => "Not initialized",
Self::InvalidArgument => "Invalid argument",
Self::InvalidAddress => "Invalid address",
Self::InvalidOperation => "Invalid operation",
Self::Cmd2IO => "CMD2 I/O",
Self::Cmd3IO => "CMD3 I/O",
Self::Cmd6CheckIO => "CMD6 I/O (check)",
Self::Cmd6CheckCRC => "CMD6 CRC (check)",
Self::Cmd6CheckTimeout => "CMD6 timeout (check)",
Self::Cmd6CheckResponse => "CMD6 invalid response (check)",
Self::Cmd6SwitchIO => "CMD6 I/O (switch)",
Self::Cmd6SwitchCRC => "CMD6 CRC (switch)",
Self::Cmd6SwitchTimeout => "CMD6 timeout (switch)",
Self::Cmd6SwitchResponse => "CMD6 invalid response (switch)",
Self::Cmd7IO => "CMD7 I/O",
Self::Cmd8IO => "CMD8 I/O",
Self::Cmd9IO => "CMD9 I/O",
Self::Cmd10IO => "CMD10 I/O",
Self::Cmd18IO => "CMD18 I/O",
Self::Cmd18CRC => "CMD18 CRC",
Self::Cmd18Timeout => "CMD18 timeout",
Self::Cmd25IO => "CMD25 I/O",
Self::Cmd25CRC => "CMD25 CRC",
Self::Cmd25Timeout => "CMD25 timeout",
Self::Acmd6IO => "ACMD6 I/O",
Self::Acmd41IO => "ACMD41 I/O",
Self::Acmd41OCR => "ACMD41 OCR",
Self::Acmd41Timeout => "ACMD41 timeout",
Self::Locked => "SD card is locked by the N64 side",
})
}
}
impl TryFrom<Vec<u8>> for SdCardResult {
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() < 4 {
return Err(Error::new(
"Incorrect data length for SD card result data packet",
));
}
Ok(match u32::from_be_bytes(value[0..4].try_into().unwrap()) {
0 => Self::OK,
1 => Self::NoCardInSlot,
2 => Self::NotInitialized,
3 => Self::InvalidArgument,
4 => Self::InvalidAddress,
5 => Self::InvalidOperation,
6 => Self::Cmd2IO,
7 => Self::Cmd3IO,
8 => Self::Cmd6CheckIO,
9 => Self::Cmd6CheckCRC,
10 => Self::Cmd6CheckTimeout,
11 => Self::Cmd6CheckResponse,
12 => Self::Cmd6SwitchIO,
13 => Self::Cmd6SwitchCRC,
14 => Self::Cmd6SwitchTimeout,
15 => Self::Cmd6SwitchResponse,
16 => Self::Cmd7IO,
17 => Self::Cmd8IO,
18 => Self::Cmd9IO,
19 => Self::Cmd10IO,
20 => Self::Cmd18IO,
21 => Self::Cmd18CRC,
22 => Self::Cmd18Timeout,
23 => Self::Cmd25IO,
24 => Self::Cmd25CRC,
25 => Self::Cmd25Timeout,
26 => Self::Acmd6IO,
27 => Self::Acmd41IO,
28 => Self::Acmd41OCR,
29 => Self::Acmd41Timeout,
30 => Self::Locked,
_ => return Err(Error::new("Unknown SD card result code")),
})
}
}
pub struct SdCardStatus {
pub byte_swap: bool,
pub clock_mode_50mhz: bool,
pub card_type_block: bool,
pub card_initialized: bool,
pub card_inserted: bool,
}
impl Display for SdCardStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.card_initialized {
f.write_str("Initialized, ")?;
f.write_str(if self.clock_mode_50mhz {
"50 MHz"
} else {
"25 MHz"
})?;
if !self.card_type_block {
f.write_str(", byte addressed")?;
}
if self.byte_swap {
f.write_str(", byte swap enabled")?;
}
} else if self.card_inserted {
f.write_str("Not initialized")?;
} else {
f.write_str("Not inserted")?;
}
Ok(())
}
}
pub struct SdCardOpPacket {
pub result: SdCardResult,
pub status: SdCardStatus,
}
impl TryFrom<Vec<u8>> for SdCardStatus {
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() != 8 {
return Err(Error::new(
"Incorrect data length for SD card status data packet",
));
}
let status = u32::from_be_bytes(value[4..8].try_into().unwrap());
return Ok(Self {
byte_swap: status & (1 << 4) != 0,
clock_mode_50mhz: status & (1 << 3) != 0,
card_type_block: status & (1 << 2) != 0,
card_initialized: status & (1 << 1) != 0,
card_inserted: status & (1 << 0) != 0,
});
}
}
pub struct SdCardInfo {
pub sectors: u64,
}
impl TryFrom<Vec<u8>> for SdCardInfo {
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() != 32 {
return Err(Error::new(
"Incorrect data length for SD card info data packet",
));
}
let csd = u128::from_be_bytes(value[0..16].try_into().unwrap());
let csd_structure = (csd >> 126) & 0x3;
match csd_structure {
0 => {
let c_size = ((csd >> 62) & 0xFFF) as u64;
let c_size_mult = ((csd >> 47) & 0x7) as u32;
let read_bl_len = ((csd >> 80) & 0xF) as u32;
Ok(SdCardInfo {
sectors: (c_size + 1) * 2u64.pow(c_size_mult + 2) * 2u64.pow(read_bl_len) / 512,
})
}
1 => {
let c_size = ((csd >> 48) & 0x3FFFFF) as u64;
Ok(SdCardInfo {
sectors: (c_size + 1) * 1024,
})
}
2 => {
let c_size = ((csd >> 48) & 0xFFFFFFF) as u64;
Ok(SdCardInfo {
sectors: (c_size + 1) * 1024,
})
}
_ => Err(Error::new("Unknown CSD structure value")),
}
}
}
pub struct DiskBlock { pub struct DiskBlock {
pub address: u32, pub address: u32,
pub track: u32, pub track: u32,