From 74e20cb8cced39f62d05f942e21e74e6ff2a6feb Mon Sep 17 00:00:00 2001 From: Mateusz Faderewski Date: Sun, 29 Sep 2024 20:58:52 +0200 Subject: [PATCH] [SC64][SW] Added access to the SD card via USB interface (#90) --- README.md | 1 + docs/00_quick_startup_guide.md | 91 ++--- docs/01_memory_map.md | 10 +- docs/02_n64_commands.md | 50 +-- docs/03_usb_interface.md | 173 +++++++-- fw/rtl/memory/memory_bram.sv | 22 +- sw/bootloader/src/fatfs/ffconf.h | 6 +- sw/bootloader/src/sc64.c | 1 + sw/bootloader/src/sc64.h | 1 + sw/controller/src/cfg.c | 86 +++-- sw/controller/src/dd.c | 26 +- sw/controller/src/flashram.c | 2 +- sw/controller/src/sd.c | 26 +- sw/controller/src/sd.h | 11 + sw/controller/src/usb.c | 119 +++++- sw/controller/src/writeback.c | 11 +- sw/deployer/Cargo.lock | 124 ++++-- sw/deployer/Cargo.toml | 4 + sw/deployer/build.rs | 19 + sw/deployer/src/main.rs | 255 ++++++++++++- sw/deployer/src/sc64/error.rs | 6 + sw/deployer/src/sc64/ff.rs | 631 +++++++++++++++++++++++++++++++ sw/deployer/src/sc64/mod.rs | 190 +++++++++- sw/deployer/src/sc64/types.rs | 235 ++++++++++++ 24 files changed, 1897 insertions(+), 203 deletions(-) create mode 100644 sw/deployer/build.rs create mode 100644 sw/deployer/src/sc64/ff.rs diff --git a/README.md b/README.md index 481e618..bd5f2f7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - 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 - 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 - Initial programming via UART header or dedicated JTAG/SWD interfaces - Software and firmware updatable via USB interface diff --git a/docs/00_quick_startup_guide.md b/docs/00_quick_startup_guide.md index 36f7216..b15a174 100644 --- a/docs/00_quick_startup_guide.md +++ b/docs/00_quick_startup_guide.md @@ -1,50 +1,38 @@ -- [First time setup on PC](#first-time-setup-on-pc) -- [Firmware backup/update](#firmware-backupupdate) -- [Running menu in standalone mode](#running-menu-in-standalone-mode) -- [Uploading game and/or save from PC](#uploading-game-andor-save-from-pc) -- [Downloading save to PC](#downloading-save-to-pc) -- [Running 64DD games from PC](#running-64dd-games-from-pc) -- [Direct boot option](#direct-boot-option) -- [Debug terminal on PC](#debug-terminal-on-pc) +- [First time setup](#first-time-setup) + - [Standalone mode (Running menu and games on the N64)](#standalone-mode-running-menu-and-games-on-the-n64) + - [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) + - [Downloading save to PC](#downloading-save-to-pc) + - [Running 64DD games from PC](#running-64dd-games-from-pc) + - [Direct boot option](#direct-boot-option) + - [Debug terminal on PC](#debug-terminal-on-pc) + - [Firmware backup/update](#firmware-backupupdate) - [LED blink patters](#led-blink-patters) --- -## First time setup on PC +# First time setup -**Windows platform: replace `./sc64deployer` in examples below with `sc64deployer.exe`** - -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 +## Standalone mode (Running menu and games on the N64) 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. +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. --- -## 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` @@ -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. 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` Replace `path_to_save.sav` with appropriate value. 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. 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. 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. 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). 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. 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: | 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 | | 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 | @@ -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. 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. diff --git a/docs/01_memory_map.md b/docs/01_memory_map.md index add661a..d2fa04e 100644 --- a/docs/01_memory_map.md +++ b/docs/01_memory_map.md @@ -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 | | Data buffer | `0x0500_0000` | 8 kiB | RW | BlockRAM | | EEPROM | `0x0500_2000` | 2 kiB | RW | BlockRAM | -| 64DD buffer | `0x0500_2800` | 256 bytes | RW | BlockRAM | -| FlashRAM buffer [2] | `0x0500_2900` | 128 bytes | R | BlockRAM | -| N/A [3] | `0x0500_2980` | to `0x07FF_FFFF` | R | N/A | +| 64DD/MCU buffer | `0x0500_2800` | 1 kiB | RW | BlockRAM | +| FlashRAM buffer [2] | `0x0500_2C00` | 128 bytes | R | BlockRAM | +| 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 [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 | | 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 | -| 64DD buffer [8] | `0x1FFE_2800` | 256 bytes | 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 | +| 64DD/MCU buffer [8] | `0x1FFE_2800` | 1 kiB | RW | `0x0500_2800` | 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 | - 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. diff --git a/docs/02_n64_commands.md b/docs/02_n64_commands.md index a832454..b8d9a71 100644 --- a/docs/02_n64_commands.md +++ b/docs/02_n64_commands.md @@ -4,28 +4,28 @@ ## N64 commands -| id | name | arg0 | arg1 | rsp0 | rsp1 | description | -| --- | --------------------- | ------------- | ------------ | ---------------- | -------------- | ---------------------------------------------------------- | -| `v` | **IDENTIFIER_GET** | --- | --- | identifier | --- | Get flashcart identifier `SCv2` | -| `V` | **VERSION_GET** | --- | --- | major/minor | revision | Get flashcart firmware version | -| `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 | -| `a` | **SETTING_GET** | setting_id | --- | --- | current_value | Get 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_SET** | time_0 | time_1 | --- | --- | Set new RTC value | -| `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 | -| `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 | -| `i` | **SD_CARD_OP** | pi_address | operation | --- | return_data | Perform special operation on SD card | -| `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_WRITE** | pi_address | sector_count | --- | --- | Write sectors from flashcart to SD card | -| `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_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 | -| `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 | -| `%` | **DIAGNOSTIC_GET** | diagnostic_id | --- | --- | value | Get diagnostic data | +| id | name | arg0 | arg1 | rsp0 | rsp1 | description | +| --- | --------------------- | ------------- | ------------ | ---------------- | -------------- | ------------------------------------------------------------ | +| `v` | **IDENTIFIER_GET** | --- | --- | identifier | --- | Get flashcart identifier `SCv2` | +| `V` | **VERSION_GET** | --- | --- | major/minor | revision | Get flashcart firmware version | +| `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 | +| `a` | **SETTING_GET** | setting_id | --- | --- | current_value | Get 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_SET** | time_0 | time_1 | --- | --- | Set new RTC value | +| `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 | +| `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 | +| `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 | +| `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 the flashcart memory space to the SD card | +| `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_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 | +| `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 | +| `%` | **DIAGNOSTIC_GET** | diagnostic_id | --- | --- | value | Get diagnostic data | diff --git a/docs/03_usb_interface.md b/docs/03_usb_interface.md index 5f3d82c..52be14b 100644 --- a/docs/03_usb_interface.md +++ b/docs/03_usb_interface.md @@ -45,6 +45,22 @@ - [`data` (data)](#data-data-1) - [`X`: **AUX\_WRITE**](#x-aux_write) - [`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) - [`arg0` (error)](#arg0-error) - [`W`: **WRITEBACK\_ENABLE**](#w-writeback_enable) @@ -152,30 +168,33 @@ Available packet IDs are listed in the [asynchronous packets](#asynchronous-pack ## Supported commands -| id | name | arg0 | arg1 | data | response | description | -| --- | ----------------------------------------------- | ------------ | ------------- | ---- | ---------------- | -------------------------------------------------------------- | -| `v` | [**IDENTIFIER_GET**](#v-identifier_get) | --- | --- | --- | identifier | Get flashcart identifier `SCv2` | -| `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) | -| `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_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_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_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_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!) | -| `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 | -| `W` | [**WRITEBACK_ENABLE**](#w-writeback_enable) | --- | --- | --- | --- | Enable save writeback through USB packet | -| `p` | **FLASH_WAIT_BUSY** | wait | --- | --- | erase_block_size | Wait until flash ready / Get flash block erase size | -| `P` | **FLASH_ERASE_BLOCK** | address | --- | --- | --- | Start flash block erase | -| `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 | +| id | name | arg0 | arg1 | data | response | description | +| --- | ----------------------------------------------- | ------------ | ------------- | ------ | ---------------- | -------------------------------------------------------------- | +| `v` | [**IDENTIFIER_GET**](#v-identifier_get) | --- | --- | --- | identifier | Get flashcart identifier `SCv2` | +| `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) | +| `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_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_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_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_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!) | +| `X` | [**AUX_WRITE**](#x-aux_write) | data | --- | --- | --- | Send small auxiliary data to be received by app running on N64 | +| `i` | [**SD_CARD_OP**](#i-sd_card_op) | address | operation | --- | result/status | Perform special operation on the SD card | +| `s` | [**SD_READ**](#s-sd_read) | address | sector_count | sector | result | Read sectors from the SD card to flashcart memory space | +| `S` | [**SD_WRITE**](#s-sd_write) | address | sector_count | sector | result | Write sectors from the flashcart memory space to the SD card | +| `D` | [**DD_SET_BLOCK_READY**](#d-dd_set_block_ready) | error | --- | --- | --- | Notify flashcart about 64DD block readiness | +| `W` | [**WRITEBACK_ENABLE**](#w-writeback_enable) | --- | --- | --- | --- | Enable save writeback through USB packet | +| `p` | **FLASH_WAIT_BUSY** | wait | --- | --- | erase_block_size | Wait until flash ready / Get flash block erase size | +| `P` | **FLASH_ERASE_BLOCK** | address | --- | --- | --- | Start flash block erase | +| `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 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** **Notify flashcart about 64DD block readiness** diff --git a/fw/rtl/memory/memory_bram.sv b/fw/rtl/memory/memory_bram.sv index 689dabf..7869219 100644 --- a/fw/rtl/memory/memory_bram.sv +++ b/fw/rtl/memory/memory_bram.sv @@ -36,11 +36,11 @@ module memory_bram ( eeprom_selected = 1'b0; dd_selected = 1'b0; flashram_selected = 1'b0; - if (mem_bus.address[25:24] == 2'b01 && mem_bus.address[23:14] == 10'd0) begin - buffer_selected = mem_bus.address[13] == 1'b0; - eeprom_selected = mem_bus.address[13:11] == 3'b100; - dd_selected = mem_bus.address[13:8] == 6'b101000; - flashram_selected = mem_bus.address[13:7] == 7'b1010010; + if (mem_bus.address[26:24] == 3'h5) begin + buffer_selected = (mem_bus.address[23:0] >= 24'h00_0000 && mem_bus.address[23:0] < 24'h00_2000); + eeprom_selected = (mem_bus.address[23:0] >= 24'h00_2000 && mem_bus.address[23:0] < 24'h00_2800); + dd_selected = (mem_bus.address[23:0] >= 24'h00_2800 && mem_bus.address[23:0] < 24'h00_2C00); + flashram_selected = (mem_bus.address[23:0] >= 24'h00_2C00 && mem_bus.address[23:0] < 24'h00_2C80); end end @@ -112,26 +112,26 @@ module memory_bram ( 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; always_ff @(posedge clk) 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 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 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 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 diff --git a/sw/bootloader/src/fatfs/ffconf.h b/sw/bootloader/src/fatfs/ffconf.h index 00a3818..ad41152 100644 --- a/sw/bootloader/src/fatfs/ffconf.h +++ b/sw/bootloader/src/fatfs/ffconf.h @@ -30,7 +30,7 @@ / 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) */ @@ -133,7 +133,7 @@ / 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. / / 0: ANSI/OEM in current CP (TCHAR = char) @@ -145,7 +145,7 @@ / 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 /* 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 diff --git a/sw/bootloader/src/sc64.c b/sw/bootloader/src/sc64.c index c6a71dc..7fd7a8d 100644 --- a/sw/bootloader/src/sc64.c +++ b/sw/bootloader/src/sc64.c @@ -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_OCR: return "ACMD41 OCR"; 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)"; } } diff --git a/sw/bootloader/src/sc64.h b/sw/bootloader/src/sc64.h index 90e4b42..1c684c6 100644 --- a/sw/bootloader/src/sc64.h +++ b/sw/bootloader/src/sc64.h @@ -55,6 +55,7 @@ typedef enum { SD_ERROR_ACMD41_IO = 27, SD_ERROR_ACMD41_OCR = 28, SD_ERROR_ACMD41_TIMEOUT = 29, + SD_ERROR_LOCKED = 30, } sc64_sd_error_t; typedef uint32_t sc64_error_t; diff --git a/sw/controller/src/cfg.c b/sw/controller/src/cfg.c index 99317c2..d7db3f8 100644 --- a/sw/controller/src/cfg.c +++ b/sw/controller/src/cfg.c @@ -141,6 +141,10 @@ static struct process p; 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); if (reg & CFG_CMD_AUX_PENDING) { @@ -634,21 +638,25 @@ void cfg_process (void) { p.data[0] = p.usb_output_ready ? 0 : (1 << 31); break; - case CMD_ID_SD_CARD_OP: + case CMD_ID_SD_CARD_OP: { + sd_error_t error = SD_OK; switch (p.data[1]) { case SD_CARD_OP_DEINIT: - sd_card_deinit(); - break; - - case SD_CARD_OP_INIT: { - led_activity_on(); - sd_error_t error = sd_card_init(); - led_activity_off(); - if (error != SD_OK) { - return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); + error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + sd_card_deinit(); + sd_release_lock(SD_LOCK_N64); + } + break; + + case SD_CARD_OP_INIT: + error = sd_try_lock(SD_LOCK_N64); + if (error == SD_OK) { + led_activity_on(); + error = sd_card_init(); + led_activity_off(); } break; - } case SD_CARD_OP_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))) { return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); } - sd_error_t error = sd_card_get_info(p.data[0]); - if (error != SD_OK) { - return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); + error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + error = sd_card_get_info(p.data[0]); } break; - case SD_CARD_OP_BYTE_SWAP_ON: { - sd_error_t error = sd_set_byte_swap(true); - if (error != SD_OK) { - return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); + case SD_CARD_OP_BYTE_SWAP_ON: + error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + error = sd_set_byte_swap(true); } break; - } - case SD_CARD_OP_BYTE_SWAP_OFF: { - sd_error_t error = sd_set_byte_swap(false); - if (error != SD_OK) { - return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); + case SD_CARD_OP_BYTE_SWAP_OFF: + error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + error = sd_set_byte_swap(false); } break; - } 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; + } - 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]; break; + } case CMD_ID_SD_READ: { 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))) { return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); } - led_activity_on(); - sd_error_t error = sd_read_sectors(p.data[0], p.sd_card_sector, p.data[1]); - led_activity_off(); + sd_error_t error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + led_activity_on(); + error = sd_read_sectors(p.data[0], p.sd_card_sector, p.data[1]); + led_activity_off(); + } if (error != SD_OK) { 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))) { return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, SD_ERROR_INVALID_ADDRESS); } - led_activity_on(); - sd_error_t error = sd_write_sectors(p.data[0], p.sd_card_sector, p.data[1]); - led_activity_off(); + sd_error_t error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + led_activity_on(); + error = sd_write_sectors(p.data[0], p.sd_card_sector, p.data[1]); + led_activity_off(); + } if (error != SD_OK) { return cfg_cmd_reply_error(ERROR_TYPE_SD_CARD, error); } diff --git a/sw/controller/src/dd.c b/sw/controller/src/dd.c index aad3ae9..e842622 100644 --- a/sw/controller/src/dd.c +++ b/sw/controller/src/dd.c @@ -134,11 +134,14 @@ static bool dd_block_read_request (void) { uint16_t index = dd_track_head_block(); uint32_t buffer_address = DD_BLOCK_BUFFER_ADDRESS; if (p.sd_mode) { - uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; - uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, false); - led_activity_on(); - sd_error_t error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_read_sectors); - led_activity_off(); + sd_error_t error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; + uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, false); + 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); } else { 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 buffer_address = DD_BLOCK_BUFFER_ADDRESS; if (p.sd_mode) { - uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; - uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, true); - led_activity_on(); - sd_error_t error = sd_optimize_sectors(buffer_address, sector_table, sectors, sd_write_sectors); - led_activity_off(); + sd_error_t error = sd_get_lock(SD_LOCK_N64); + if (error == SD_OK) { + uint32_t sector_table[DD_SD_SECTOR_TABLE_SIZE]; + uint32_t sectors = dd_fill_sd_sector_table(index, sector_table, true); + 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); } else { usb_tx_info_t packet_info; diff --git a/sw/controller/src/flashram.c b/sw/controller/src/flashram.c index 60bcb91..4372528 100644 --- a/sw/controller/src/flashram.c +++ b/sw/controller/src/flashram.c @@ -6,7 +6,7 @@ #define FLASHRAM_SECTOR_SIZE (16 * 1024) #define FLASHRAM_PAGE_SIZE (128) #define FLASHRAM_ADDRESS (0x03FE0000UL) -#define FLASHRAM_BUFFER_ADDRESS (0x05002900UL) +#define FLASHRAM_BUFFER_ADDRESS (0x05002C00UL) typedef enum { diff --git a/sw/controller/src/sd.c b/sw/controller/src/sd.c index f6e94b3..72d0411 100644 --- a/sw/controller/src/sd.c +++ b/sw/controller/src/sd.c @@ -4,7 +4,7 @@ #include "timer.h" -#define SD_INIT_BUFFER_ADDRESS (0x05002800UL) +#define SD_INIT_BUFFER_ADDRESS (0x05002BB8UL) #define BYTE_SWAP_ADDRESS_END (0x05000000UL) #define CMD6_ARG_CHECK_HS (0x00FFFFF1UL) @@ -86,6 +86,7 @@ struct process { uint8_t csd[16]; uint8_t cid[16]; bool byte_swap; + sd_lock_t lock; }; @@ -428,6 +429,7 @@ sd_error_t sd_card_init (void) { void sd_card_deinit (void) { if (p.card_initialized) { p.card_initialized = false; + p.card_type_block = false; p.byte_swap = false; sd_set_clock(CLOCK_400KHZ); 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; } +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) { p.card_initialized = false; p.byte_swap = false; + p.lock = SD_LOCK_NONE; sd_set_clock(CLOCK_STOP); } diff --git a/sw/controller/src/sd.h b/sw/controller/src/sd.h index d3e326d..a6c8b03 100644 --- a/sw/controller/src/sd.h +++ b/sw/controller/src/sd.h @@ -41,10 +41,17 @@ typedef enum { SD_ERROR_ACMD41_IO = 27, SD_ERROR_ACMD41_OCR = 28, SD_ERROR_ACMD41_TIMEOUT = 29, + SD_ERROR_LOCKED = 30, } sd_error_t; 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); 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_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_process (void); diff --git a/sw/controller/src/usb.c b/sw/controller/src/usb.c index 8a61d04..70a1a1a 100644 --- a/sw/controller/src/usb.c +++ b/sw/controller/src/usb.c @@ -4,7 +4,9 @@ #include "flash.h" #include "fpga.h" #include "hw.h" +#include "led.h" #include "rtc.h" +#include "sd.h" #include "timer.h" #include "update.h" #include "usb.h" @@ -15,7 +17,7 @@ #define BOOTLOADER_ADDRESS (0x04E00000UL) #define BOOTLOADER_LENGTH (1920 * 1024) -#define MEMORY_LENGTH (0x05002980UL) +#define MEMORY_LENGTH (0x05002C80UL) #define RX_FLUSH_ADDRESS (0x07F00000UL) #define RX_FLUSH_LENGTH (1 * 1024 * 1024) @@ -268,6 +270,7 @@ static void usb_rx_process (void) { case 'R': cfg_reset_state(); cic_reset_parameters(); + sd_release_lock(SD_LOCK_USB); p.rx_state = RX_STATE_IDLE; p.response_pending = true; break; @@ -384,6 +387,119 @@ static void usb_rx_process (void) { p.response_pending = true; 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(§or)) { + 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(§or)) { + 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': dd_set_block_ready(p.rx_args[0] == 0); p.rx_state = RX_STATE_IDLE; @@ -635,5 +751,6 @@ void usb_process (void) { usb_tx_process(); } else { usb_flush_packet(); + sd_release_lock(SD_LOCK_USB); } } diff --git a/sw/controller/src/writeback.c b/sw/controller/src/writeback.c index f089d96..6c943fc 100644 --- a/sw/controller/src/writeback.c +++ b/sw/controller/src/writeback.c @@ -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) { - save_type_t save; uint32_t address; uint32_t length; - save = writeback_get_address_length(&address, &length); - if (save == SAVE_TYPE_NONE) { + if (writeback_get_address_length(&address, &length) == SAVE_TYPE_NONE) { writeback_disable(); 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(); return; } diff --git a/sw/deployer/Cargo.lock b/sw/deployer/Cargo.lock index e09cfe6..85c87c8 100644 --- a/sw/deployer/Cargo.lock +++ b/sw/deployer/Cargo.lock @@ -124,7 +124,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -162,6 +162,26 @@ dependencies = [ "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]] name = "bit_field" version = "0.10.2" @@ -218,13 +238,22 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.0.103" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2755ff20a1d93490d26ba33a6f092a38a508398a5320df5d4b3014fcccce9410" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "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]] @@ -263,6 +292,17 @@ dependencies = [ "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]] name = "clap" version = "4.5.8" @@ -303,7 +343,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -482,6 +522,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "half" version = "2.4.1" @@ -633,7 +679,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -755,6 +801,16 @@ dependencies = [ "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]] name = "libudev" version = "0.3.0" @@ -922,7 +978,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -1007,6 +1063,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -1038,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -1219,10 +1285,18 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "sc64deployer" version = "2.19.0" dependencies = [ + "bindgen", + "cc", "chrono", "clap", "clap-num", @@ -1266,14 +1340,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -1308,6 +1382,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1357,9 +1437,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1402,7 +1482,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -1430,18 +1510,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", @@ -1527,7 +1607,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -1549,7 +1629,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1738,9 +1818,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/sw/deployer/Cargo.toml b/sw/deployer/Cargo.toml index 20b0378..56c7459 100644 --- a/sw/deployer/Cargo.toml +++ b/sw/deployer/Cargo.toml @@ -6,6 +6,10 @@ authors = ["Polprzewodnikowy"] description = "SummerCart64 loader and control software" documentation = "https://github.com/Polprzewodnikowy/SummerCart64" +[build-dependencies] +bindgen = "0.70.1" +cc = "1.1.18" + [dependencies] chrono = "0.4.38" clap = { version = "4.5.8", features = ["derive"] } diff --git a/sw/deployer/build.rs b/sw/deployer/build.rs new file mode 100644 index 0000000..98a0874 --- /dev/null +++ b/sw/deployer/build.rs @@ -0,0 +1,19 @@ +fn main() -> Result<(), Box> { + 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(()) +} diff --git a/sw/deployer/src/main.rs b/sw/deployer/src/main.rs index 3915324..7ed78e1 100644 --- a/sw/deployer/src/main.rs +++ b/sw/deployer/src/main.rs @@ -11,12 +11,13 @@ use panic_message::panic_message; use std::{ fs::File, io::{stdin, stdout, Read, Write}, + panic, path::PathBuf, + process, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, - {panic, process}, }; #[derive(Parser)] @@ -57,6 +58,12 @@ enum Commands { /// Dump data from arbitrary location in SC64 memory space Dump(DumpArgs), + /// Perform operations on the SD card + SD { + #[command(subcommand)] + command: SDCommands, + }, + /// Print information about connected SC64 device Info, @@ -202,6 +209,71 @@ struct DumpArgs { path: PathBuf, } +#[derive(Subcommand)] +enum SDCommands { + /// List a directory on the SD card + #[command(name = "ls")] + List { + /// Path to the directory + path: Option, + }, + + /// 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, + }, + + /// 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, + }, + + /// Format the SD card + #[command(name = "mkfs")] + Format, +} + #[derive(Subcommand)] enum SetCommands { /// Synchronize real time clock (RTC) on the SC64 with local system time @@ -318,6 +390,9 @@ fn main() { 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)) { Ok(_) => {} Err(payload) => { @@ -340,6 +415,7 @@ fn handle_command(command: &Commands, port: Option, remote: Option handle_64dd_command(connection, args), Commands::Debug(args) => handle_debug_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::Reset => handle_reset_command(connection), Commands::Set { command } => handle_set_command(connection, command), @@ -767,16 +843,167 @@ fn handle_dump_command(connection: Connection, args: &DumpArgs) -> Result<(), sc 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> { let mut sc64 = init_sc64(connection, true)?; let (major, minor, revision) = sc64.check_firmware_version()?; let state = sc64.get_device_state()?; - let datetime = state.datetime.format("%Y-%m-%d %H:%M:%S"); println!("{}", "SummerCart64 state information:".bold()); println!(" Firmware version: v{}.{}.{}", major, minor, revision); - println!(" RTC datetime: {}", datetime); + println!(" RTC datetime: {}", state.datetime); println!(" Boot mode: {}", state.boot_mode); println!(" Save type: {}", state.save_type); 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!(" LED blink: {}", state.led_enable); println!(" IS-Viewer 64: {}", state.isviewer); + println!(" SD card status: {}", state.sd_card_status); println!("{}", "SummerCart64 diagnostic information:".bold()); println!(" PI I/O access: {}", state.fpga_debug_data.pi_io_access); println!( @@ -921,19 +1149,30 @@ fn handle_firmware_command( } 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()); print!(" Performing USB read speed test... "); stdout().flush().unwrap(); - let read_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Read)?; - println!("{}", format!("{read_speed:.2} MiB/s",).bright_green()); + let usb_read_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Read)?; + println!("{}", format!("{usb_read_speed:.2} MiB/s",).bright_green()); print!(" Performing USB write speed test... "); stdout().flush().unwrap(); - let write_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Write)?; - println!("{}", format!("{write_speed:.2} MiB/s",).bright_green()); + let usb_write_speed = sc64.test_usb_speed(sc64::SpeedTestDirection::Write)?; + 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()); diff --git a/sw/deployer/src/sc64/error.rs b/sw/deployer/src/sc64/error.rs index 0702fd5..0a23b9c 100644 --- a/sw/deployer/src/sc64/error.rs +++ b/sw/deployer/src/sc64/error.rs @@ -26,3 +26,9 @@ impl From for Error { Error::new(format!("IO error: {}", value).as_str()) } } + +impl From for Error { + fn from(value: super::ff::Error) -> Self { + Error::new(format!("FatFs error: {}", value).as_str()) + } +} diff --git a/sw/deployer/src/sc64/ff.rs b/sw/deployer/src/sc64/ff.rs new file mode 100644 index 0000000..855bddf --- /dev/null +++ b/sw/deployer/src/sc64/ff.rs @@ -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 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>(path: P) -> Result { + 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>> = 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, +} + +impl FatFs { + pub fn new(driver: impl FFDriver + 'static) -> Result { + 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>(&mut self, path: P) -> Result { + File::open( + path, + fatfs::FA_OPEN_EXISTING | fatfs::FA_READ | fatfs::FA_WRITE, + ) + } + + pub fn create>(&mut self, path: P) -> Result { + File::open( + path, + fatfs::FA_CREATE_ALWAYS | fatfs::FA_READ | fatfs::FA_WRITE, + ) + } + + pub fn stat>(&mut self, path: P) -> Result { + 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>(&mut self, path: P) -> Result { + Directory::open(path) + } + + pub fn list>(&mut self, path: P) -> Result, 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>(&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>(&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>(&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 { + 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 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>(path: P) -> Result { + 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, 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>(path: P, mode: u32) -> Result { + 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 { + 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 { + 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 { + 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) }; + } +} diff --git a/sw/deployer/src/sc64/mod.rs b/sw/deployer/src/sc64/mod.rs index 0c74005..0142bc3 100644 --- a/sw/deployer/src/sc64/mod.rs +++ b/sw/deployer/src/sc64/mod.rs @@ -1,5 +1,6 @@ mod cic; mod error; +pub mod ff; pub mod firmware; mod ftdi; mod link; @@ -13,10 +14,11 @@ pub use self::{ link::list_local_devices, server::ServerEvent, types::{ - AuxMessage, BootMode, ButtonMode, ButtonState, CicSeed, DataPacket, DdDiskState, + AuxMessage, BootMode, ButtonMode, ButtonState, CicSeed, CicStep, DataPacket, DdDiskState, DdDriveType, DdMode, DebugPacket, DiagnosticData, DiskPacket, DiskPacketKind, 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, time::{convert_from_datetime, convert_to_datetime}, 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; @@ -58,6 +61,7 @@ pub struct DeviceState { pub button_mode: ButtonMode, pub rom_extended_enable: Switch, pub led_enable: Switch, + pub sd_card_status: SdCardStatus, pub datetime: NaiveDateTime, pub fpga_debug_data: FpgaDebugData, pub diagnostic_data: DiagnosticData, @@ -94,13 +98,18 @@ const SRAM_1M_LENGTH: usize = 128 * 1024; 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_FLASH: u32 = 0x0410_0000; // Arbitrary offset in Flash memory const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90); 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; @@ -235,6 +244,53 @@ impl SC64 { Ok(()) } + fn command_sd_card_operation(&mut self, op: SdCardOp) -> Result { + 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 { + let data = self.link.execute_command_raw( + b's', + [address, count], + §or.to_be_bytes(), + false, + true, + )?; + Ok(data.try_into()?) + } + + fn command_sd_card_write( + &mut self, + address: u32, + sector: u32, + count: u32, + ) -> Result { + let data = self.link.execute_command_raw( + b'S', + [address, count], + §or.to_be_bytes(), + false, + true, + )?; + Ok(data.try_into()?) + } + fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> { self.link.execute_command(b'D', [error as u32, 0], &[])?; Ok(()) @@ -489,6 +545,7 @@ impl SC64 { button_mode: get_config!(self, ButtonMode)?, rom_extended_enable: get_config!(self, RomExtendedEnable)?, led_enable: get_setting!(self, LedEnable)?, + sd_card_status: self.get_sd_card_status()?, datetime: self.get_datetime()?, fpga_debug_data: self.command_fpga_debug_data_get()?, diagnostic_data: self.command_diagnostic_data_get()?, @@ -606,6 +663,83 @@ impl SC64 { Ok(false) } + pub fn init_sd_card(&mut self) -> Result { + self.command_sd_card_operation(SdCardOp::Init) + .map(|packet| packet.result) + } + + pub fn deinit_sd_card(&mut self) -> Result { + self.command_sd_card_operation(SdCardOp::Deinit) + .map(|packet| packet.result) + } + + pub fn get_sd_card_status(&mut self) -> Result { + self.command_sd_card_operation(SdCardOp::GetStatus) + .map(|reply| reply.status) + } + + pub fn get_sd_card_info(&mut self) -> Result { + 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 { + 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 { + 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> { let identifier = self.command_identifier_get().map_err(|e| { Error::new(format!("Couldn't get SC64 device identifier: {e}").as_str()) @@ -634,6 +768,14 @@ impl SC64 { self.command_state_reset() } + pub fn is_console_powered_on(&mut self) -> Result { + 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, Error> { self.command_state_reset()?; 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()) } + pub fn test_sd_card(&mut self) -> Result { + 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( &mut self, pattern: MemoryTestPattern, diff --git a/sw/deployer/src/sc64/types.rs b/sw/deployer/src/sc64/types.rs index 585acee..541e8de 100644 --- a/sw/deployer/src/sc64/types.rs +++ b/sw/deployer/src/sc64/types.rs @@ -737,6 +737,241 @@ impl TryFrom> for DiskPacket { } } +pub enum SdCardOp { + Deinit, + Init, + GetStatus, + GetInfo(u32), + ByteSwapOn, + ByteSwapOff, +} + +impl From 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> for SdCardResult { + type Error = Error; + fn try_from(value: Vec) -> Result { + 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> for SdCardStatus { + type Error = Error; + fn try_from(value: Vec) -> Result { + 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> for SdCardInfo { + type Error = Error; + fn try_from(value: Vec) -> Result { + 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 address: u32, pub track: u32,