mirror of
https://github.com/Polprzewodnikowy/SummerCart64.git
synced 2024-11-22 05:59:15 +01:00
This commit is contained in:
parent
a3b2819803
commit
1e19b648fb
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -5,3 +5,4 @@ hw/pcb/*.html linguist-generated
|
|||||||
sw/bootloader/src/fatfs/* linguist-vendored
|
sw/bootloader/src/fatfs/* linguist-vendored
|
||||||
sw/bootloader/src/fatfs/diskio.c -linguist-vendored
|
sw/bootloader/src/fatfs/diskio.c -linguist-vendored
|
||||||
sw/controller/inc/* linguist-vendored
|
sw/controller/inc/* linguist-vendored
|
||||||
|
sw/deployer/data/* linguist-vendored
|
||||||
|
103
.github/workflows/build.yml
vendored
103
.github/workflows/build.yml
vendored
@ -13,65 +13,65 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-fw-hw-sw:
|
build-firmware:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download SummerCart64 repository
|
- name: Download SummerCart64 repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set SC64 version
|
- name: Set package version
|
||||||
uses: frabert/replace-string-action@v2
|
uses: frabert/replace-string-action@v2
|
||||||
id: sc64version
|
id: version
|
||||||
with:
|
with:
|
||||||
pattern: '\/'
|
pattern: '\/'
|
||||||
string: '-${{ github.ref_name }}'
|
string: '${{ github.ref_name }}'
|
||||||
replace-with: '-'
|
replace-with: '-'
|
||||||
|
|
||||||
- name: Build everything
|
- name: Build firmware
|
||||||
run: ./docker_build.sh release --force-clean
|
run: ./docker_build.sh release --force-clean
|
||||||
env:
|
env:
|
||||||
SC64_VERSION: ${{ steps.sc64version.outputs.replaced }}
|
SC64_VERSION: ${{ steps.version.outputs.replaced }}
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: sc64-pkg${{ steps.sc64version.outputs.replaced }}
|
name: sc64-pkg-${{ steps.version.outputs.replaced }}
|
||||||
path: |
|
path: |
|
||||||
sc64-extra${{ steps.sc64version.outputs.replaced }}.zip
|
sc64-extra-${{ steps.version.outputs.replaced }}.zip
|
||||||
sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin
|
sc64-firmware-${{ steps.version.outputs.replaced }}.bin
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
if: github.event_name == 'release' && github.event.action == 'created'
|
if: github.event_name == 'release' && github.event.action == 'created'
|
||||||
uses: softprops/action-gh-release@v0.1.15
|
uses: softprops/action-gh-release@v0.1.15
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
sc64-extra${{ steps.sc64version.outputs.replaced }}.zip
|
sc64-extra-${{ steps.version.outputs.replaced }}.zip
|
||||||
sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin
|
sc64-firmware-${{ steps.version.outputs.replaced }}.bin
|
||||||
|
|
||||||
build-sc64-py:
|
build-deployer:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
pyinstaller-build-options: --target-arch=64bit
|
executable: sc64deployer.exe
|
||||||
pyinstaller-options: --onefile --console --icon ../../assets/sc64_logo_256_256.png
|
name: sc64-deployer-windows
|
||||||
package-name: sc64-windows
|
options: -c -a -f
|
||||||
package-options: -c -a -f
|
extension: zip
|
||||||
package-extension: zip
|
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
pyinstaller-options: --onefile
|
linux-packages: libudev-dev
|
||||||
package-name: sc64-linux
|
executable: sc64deployer
|
||||||
package-options: -czf
|
name: sc64-deployer-linux
|
||||||
package-extension: tar.gz
|
options: -czf
|
||||||
|
extension: tar.gz
|
||||||
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
pyinstaller-options: --onedir --console --icon ../../assets/sc64_logo_256_256.png
|
executable: sc64deployer
|
||||||
package-name: sc64-macos
|
name: sc64-deployer-macos
|
||||||
package-options: -czf
|
options: -czf
|
||||||
package-extension: tgz
|
extension: tgz
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
@ -79,61 +79,40 @@ jobs:
|
|||||||
- name: Download SummerCart64 repository
|
- name: Download SummerCart64 repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set SC64 version
|
- name: Set package version
|
||||||
uses: frabert/replace-string-action@v2
|
uses: frabert/replace-string-action@v2
|
||||||
id: sc64version
|
id: version
|
||||||
with:
|
with:
|
||||||
pattern: '\/'
|
pattern: '\/'
|
||||||
string: '-${{ github.ref_name }}'
|
string: '${{ github.ref_name }}'
|
||||||
replace-with: '-'
|
replace-with: '-'
|
||||||
|
|
||||||
- name: Setup python
|
- name: Install linux packages
|
||||||
uses: actions/setup-python@v4
|
if: matrix.linux-packages
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Download pyinstaller repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: 'pyinstaller/pyinstaller'
|
|
||||||
ref: 'v5.8.0'
|
|
||||||
path: pyinstaller
|
|
||||||
|
|
||||||
- name: Compile and install pyinstaller
|
|
||||||
run: |
|
run: |
|
||||||
pip3 uninstall pyinstaller
|
sudo apt-get update
|
||||||
pip3 install wheel
|
sudo apt-get -y install ${{ matrix.linux-packages }}
|
||||||
pushd bootloader
|
|
||||||
python3 ./waf all ${{ matrix.pyinstaller-build-options }}
|
|
||||||
popd
|
|
||||||
pip3 install .
|
|
||||||
working-directory: pyinstaller
|
|
||||||
|
|
||||||
- name: Install sc64.py requirements
|
- name: Build deployer
|
||||||
run: pip3 install -r requirements.txt
|
run: cargo b -r
|
||||||
working-directory: sw/pc
|
working-directory: sw/deployer
|
||||||
|
|
||||||
- name: Create sc64.py executable
|
|
||||||
run: python3 -m PyInstaller --clean ${{ matrix.pyinstaller-options }} sc64.py
|
|
||||||
working-directory: sw/pc
|
|
||||||
|
|
||||||
- name: Package executable
|
- name: Package executable
|
||||||
run: |
|
run: |
|
||||||
mkdir package
|
mkdir package
|
||||||
pushd dist
|
cd target/release
|
||||||
tar ${{ matrix.package-options }} ../package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} *
|
tar ${{ matrix.options }} ../../package/${{ matrix.name }}-${{ steps.version.outputs.replaced }}.${{ matrix.extension }} ${{ matrix.executable }}
|
||||||
popd
|
working-directory: sw/deployer
|
||||||
working-directory: sw/pc
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}
|
name: ${{ matrix.name }}-${{ steps.version.outputs.replaced }}
|
||||||
path: sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }}
|
path: sw/deployer/package/${{ matrix.name }}-${{ steps.version.outputs.replaced }}.${{ matrix.extension }}
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
if: github.event_name == 'release' && github.event.action == 'created'
|
if: github.event_name == 'release' && github.event.action == 'created'
|
||||||
uses: softprops/action-gh-release@v0.1.15
|
uses: softprops/action-gh-release@v0.1.15
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }}
|
sw/deployer/package/${{ matrix.name }}-${{ steps.version.outputs.replaced }}.${{ matrix.extension }}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 53 KiB |
35
build.sh
35
build.sh
@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
SC64_VERSION=${SC64_VERSION:-"none"}
|
||||||
|
|
||||||
PACKAGE_FILE_NAME="sc64-extra"
|
PACKAGE_FILE_NAME="sc64-extra"
|
||||||
|
|
||||||
TOP_FILES=(
|
TOP_FILES=(
|
||||||
"./sw/pc/primer.py"
|
"./fw/ftdi/ft232h_config.xml"
|
||||||
"./sw/pc/requirements.txt"
|
"./sw/tools/primer.py"
|
||||||
"./sw/pc/sc64.py"
|
"./sw/tools/requirements.txt"
|
||||||
"./sw/update/sc64-firmware.bin"
|
"./sc64-firmware-${SC64_VERSION}.bin"
|
||||||
)
|
)
|
||||||
|
|
||||||
FILES=(
|
FILES=(
|
||||||
"./assets/*"
|
"./assets/*"
|
||||||
"./docs/*"
|
"./docs/*"
|
||||||
"./fw/ftdi/ft232h_config.xml"
|
|
||||||
"./hw/pcb/sc64_hw_v2.0a_bom.html"
|
"./hw/pcb/sc64_hw_v2.0a_bom.html"
|
||||||
"./hw/pcb/sc64v2.kicad_pcb"
|
"./hw/pcb/sc64v2.kicad_pcb"
|
||||||
"./hw/pcb/sc64v2.kicad_pro"
|
"./hw/pcb/sc64v2.kicad_pro"
|
||||||
@ -84,23 +85,23 @@ build_update () {
|
|||||||
build_controller
|
build_controller
|
||||||
build_fpga
|
build_fpga
|
||||||
|
|
||||||
pushd sw/update > /dev/null
|
pushd sw/tools > /dev/null
|
||||||
if [ "$FORCE_CLEAN" = true ]; then
|
if [ "$FORCE_CLEAN" = true ]; then
|
||||||
rm -f ./sc64-firmware.bin
|
rm -f ../../sc64-firmware-*.bin
|
||||||
fi
|
fi
|
||||||
GIT_INFO=""
|
GIT_INFO=""
|
||||||
if [ ! -z "${GIT_BRANCH}" ]; then GIT_INFO+="branch: [$GIT_BRANCH] "; fi
|
if [ ! -z "${SC64_VERSION}" ]; then GIT_INFO+=$'\n'"ver: $SC64_VERSION"; fi
|
||||||
if [ ! -z "${GIT_TAG}" ]; then GIT_INFO+="tag: [$GIT_TAG] "; fi
|
if [ ! -z "${GIT_BRANCH}" ]; then GIT_INFO+=$'\n'"branch: $GIT_BRANCH"; fi
|
||||||
if [ ! -z "${GIT_SHA}" ]; then GIT_INFO+="sha: [$GIT_SHA] "; fi
|
if [ ! -z "${GIT_TAG}" ]; then GIT_INFO+=$'\n'"tag: $GIT_TAG"; fi
|
||||||
if [ ! -z "${GIT_MESSAGE}" ]; then GIT_INFO+="message: [$GIT_MESSAGE] "; fi
|
if [ ! -z "${GIT_SHA}" ]; then GIT_INFO+=$'\n'"sha: $GIT_SHA"; fi
|
||||||
GIT_INFO=$(echo "$GIT_INFO" | xargs)
|
if [ ! -z "${GIT_MESSAGE}" ]; then GIT_INFO+=$'\n'"msg: $GIT_MESSAGE"; fi
|
||||||
python3 update.py \
|
python3 update.py \
|
||||||
--git "$GIT_INFO" \
|
--git "$GIT_INFO" \
|
||||||
--mcu ../controller/build/app/app.bin \
|
--mcu ../controller/build/app/app.bin \
|
||||||
--fpga ../../fw/project/lcmxo2/impl1/sc64_impl1.jed \
|
--fpga ../../fw/project/lcmxo2/impl1/sc64_impl1.jed \
|
||||||
--boot ../bootloader/build/bootloader.bin \
|
--boot ../bootloader/build/bootloader.bin \
|
||||||
--primer ../controller/build/primer/primer.bin \
|
--primer ../controller/build/primer/primer.bin \
|
||||||
sc64-firmware.bin
|
../../sc64-firmware-${SC64_VERSION}.bin
|
||||||
popd > /dev/null
|
popd > /dev/null
|
||||||
|
|
||||||
BUILT_UPDATE=true
|
BUILT_UPDATE=true
|
||||||
@ -111,15 +112,13 @@ build_release () {
|
|||||||
|
|
||||||
build_update
|
build_update
|
||||||
|
|
||||||
if [ -e "./${PACKAGE_FILE_NAME}.zip" ]; then
|
if [ -e "./${PACKAGE_FILE_NAME}-${SC64_VERSION}.zip" ]; then
|
||||||
rm -f "./${PACKAGE_FILE_NAME}.zip"
|
rm -f ./${PACKAGE_FILE_NAME}-${SC64_VERSION}.zip
|
||||||
fi
|
fi
|
||||||
PACKAGE="./${PACKAGE_FILE_NAME}${SC64_VERSION}.zip"
|
PACKAGE="./${PACKAGE_FILE_NAME}-${SC64_VERSION}.zip"
|
||||||
zip -j -r $PACKAGE ${TOP_FILES[@]}
|
zip -j -r $PACKAGE ${TOP_FILES[@]}
|
||||||
zip -r $PACKAGE ${FILES[@]}
|
zip -r $PACKAGE ${FILES[@]}
|
||||||
|
|
||||||
cp sw/update/sc64-firmware.bin ./sc64-firmware${SC64_VERSION}.bin
|
|
||||||
|
|
||||||
BUILT_RELEASE=true
|
BUILT_RELEASE=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
- [First time setup](#first-time-setup)
|
- [First time setup](#first-time-setup)
|
||||||
- [Firmware backup/update](#firmware-backupupdate)
|
- [Firmware backup/update](#firmware-backupupdate)
|
||||||
- [Internal flashcart state](#internal-flashcart-state)
|
- [Uploading game and/or save](#uploading-game-andor-save)
|
||||||
- [Uploading game/save](#uploading-gamesave)
|
|
||||||
- [Downloading save](#downloading-save)
|
- [Downloading save](#downloading-save)
|
||||||
- [Running 64DD games](#running-64dd-games)
|
- [Running 64DD games](#running-64dd-games)
|
||||||
- [Direct boot option](#direct-boot-option)
|
- [Direct boot option](#direct-boot-option)
|
||||||
@ -12,81 +11,96 @@
|
|||||||
|
|
||||||
## First time setup
|
## First time setup
|
||||||
|
|
||||||
**Windows platform: replace `./sc64` in examples below with `sc64.exe`**
|
**Windows platform: replace `./sc64deployer` in examples below with `sc64deployer.exe`**
|
||||||
|
|
||||||
1. Download the latest `sc64-{os}-{version}.{ext}` (choose OS matching your system) and `sc64-firmware-{version}.bin` from GitHub releases page
|
1. Download the latest deployer tool (`sc64-deployer-{os}-{version}.{ext}`) and firmware (`sc64-firmware-{version}.bin`) from GitHub releases page
|
||||||
2. Extract `sc64-{os}-{version}.{ext}` package contents to a folder and place `sc64-firmware-{version}.bin` inside it
|
2. Extract deployer tool package contents to a folder and place firmware file inside it
|
||||||
3. Update SC64 firmware to the latest version with `./sc64 --update-firmware sc64-firmware-{version}.bin`
|
3. Connect SC64 device to your computer with USB type C cable
|
||||||
4. Run `./sc64 --print-state` to check if update process finished successfully and SC64 is detected correctly
|
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
|
## Firmware backup/update
|
||||||
|
|
||||||
Keeping SC64 firmware up to date is highly recommended. `sc64` executable is tightly coupled with specific firmware versions and will error out when it detects unsupported firmware version.
|
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 SC64 firmware run `./sc64 --backup-firmware sc64-firmware-backup.bin`
|
To download and backup current version of the SC64 firmware run `./sc64deployer firmware backup sc64-firmware-backup.bin`
|
||||||
|
|
||||||
To update SC64 firmware run `./sc64 --update-firmware sc64-firmware-{version}.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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Internal flashcart state
|
## Uploading game and/or save
|
||||||
|
|
||||||
SC64 holds some internal configuration options after `sc64` executable finished running. To reset it simply run: `./sc64 --reset-state`. Internal flashcart state can be checked by running: `./sc64 --print-state`
|
`./sc64deployer upload path_to_rom.n64 --save-type eeprom4k --save path_to_save.sav`
|
||||||
|
|
||||||
---
|
Replace `path_to_rom.n64` / `eeprom4k` / `path_to_save.sav` with appropriate values for desired game.
|
||||||
|
Application will try to autodetect used save type so explicitly setting save type usually isn't needed.
|
||||||
## Uploading game/save
|
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.
|
||||||
`./sc64 --boot rom path_to_rom.n64 --save-type eeprom-4k --save path_to_save.sav`
|
|
||||||
|
|
||||||
Replace `path_to_rom.n64` / `eeprom-4k` / `path_to_save.sav` with appropriate values for desired game. Script will try to autodetect used save type so explicitly setting save type usually isn't needed. Check included help in program 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 fresh.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Downloading save
|
## Downloading save
|
||||||
|
|
||||||
`./sc64 --backup-save path_to_save.sav`
|
`./sc64deployer download save path_to_save.sav`
|
||||||
|
|
||||||
Replace `path_to_save.sav` with appropriate value. Specifying save type isn't required when set correctly previously.
|
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
|
## Running 64DD games
|
||||||
|
|
||||||
64DD games require DDIPL ROM and disk images. To run disk game type `./sc64 --boot ddipl --ddipl path_to_ddipl.n64 --disk path_to_disk_1.ndd --disk path_to_disk_2.ndd`.
|
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`.
|
||||||
|
|
||||||
Replace `path_to_ddipl.n64` / `path_to_disk_x.ndd` with appropriate values. Argument `--disk` can be specified multiple times. Only `.ndd` disk format is supported currently. To change inserted disk press button on the back of SC64 flashcart. Make sure retail and development disks aren't mixed together. 64DD IPL can handle only one drive type at a time.
|
Replace `path_to_ddipl.n64` / `path_to_disk_x.ndd` with appropriate values.
|
||||||
|
Multiple disk files can be passed to the command.
|
||||||
|
Only `.ndd` disk format is supported.
|
||||||
|
To change inserted disk press button on the back of SC64 device.
|
||||||
|
Make sure retail and development disks formats aren't mixed together.
|
||||||
|
64DD IPL can handle only one drive type at a time.
|
||||||
|
|
||||||
|
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.
|
If booting game through included bootloader isn't a desired option then flashcart can be put in special mode that omits this step.
|
||||||
Run `./sc64 --boot direct-rom path_to_rom.n64` 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).
|
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
|
## Debug terminal
|
||||||
|
|
||||||
`sc64` executable supports UNFLoader protocol and has same functionality implemented as aforementioned program. Use argument `--debug` to activate it.
|
`sc64deployer` application supports UNFLoader protocol and has same functionality implemented as aforementioned program.
|
||||||
|
Type `./sc64deployer debug` to activate it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 patters meaning:
|
LED on SC64 board can blink in certain situations. Most of them during normal use are related to SD card access. Here's list of blink patterns:
|
||||||
|
|
||||||
| Pattern | Meaning |
|
| Pattern | Meaning |
|
||||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| Nx [Short ON - Short OFF] | SD card is being accessed (initialization or data read/write) or save writeback is in progress |
|
| Nx [Short ON - Short OFF] | SD card access is in progress (initialization or data read/write) or save writeback is in progress |
|
||||||
| Nx [Medium ON - Long OFF] | CIC region didn't match, please power off console and power on again |
|
| Nx [Medium ON - Long OFF] | CIC region did not match, please power off console and power on again |
|
||||||
| 2x [Very short ON - Short OFF] | Pattern used during firmware update process, it means that specific part of firmware has started programming |
|
| 2x [Very short ON - Short OFF] | Pattern used during firmware update process, it means that specific part of firmware has started programming |
|
||||||
| 10x [Very short ON - Very short OFF] | Firmware has been successfully updated |
|
| 10x [Very short ON - Very short OFF] | Firmware has been successfully updated |
|
||||||
| 30x [Long ON - Long OFF] | There was serious problem during firmware update, device is most likely bricked |
|
| 30x [Long ON - Long OFF] | There was serious problem during firmware update, device is most likely bricked |
|
||||||
|
|
||||||
Nx means that blink count is varied.
|
Nx means that blink count is varied.
|
||||||
|
|
||||||
LED blinking on SD card access can be disabled through `sc64` executable. Please refer to included help for option to change the LED behavior.
|
LED blinking on SD card access can be disabled through `sc64deployer` application.
|
||||||
|
Please refer to included help for option to change the LED behavior.
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
## Step by step guide how to make SC64
|
## Step by step guide how to make SC64
|
||||||
|
|
||||||
All necessary manufacturing files are packaged in every `sc64-extra-{version}.zip` file in GitHub releases. Please download latest release before proceeding with the instructions.
|
All necessary manufacturing files are packaged in every `sc64-extra-{version}.zip` file in GitHub releases.
|
||||||
|
Please download latest release before proceeding with the instructions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -45,13 +46,16 @@ All necessary manufacturing files are packaged in every `sc64-extra-{version}.zi
|
|||||||
|
|
||||||
1. Locate `.stl` files inside `hw/shell` folder
|
1. Locate `.stl` files inside `hw/shell` folder
|
||||||
2. Use these files in the slicer and 3D printer of your choice or order ready made prints from 3D printing company
|
2. Use these files in the slicer and 3D printer of your choice or order ready made prints from 3D printing company
|
||||||
3. Find matching screws, dimensions are the same as on retail N64 cartridges
|
3. Find matching screws, go to discussions tab for community recommendations
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Putting it together**
|
### **Putting it together**
|
||||||
|
|
||||||
There are no special requirements for soldering components to board. All chips and connectors can be soldered with standard manual soldering iron, although hot air station is recommended. Interactive BOM has every component and its value highlighted on PCB drawings and it's strongly recommended to use during assembly process. You can skip this step if PCB assembly service was used in previous steps.
|
There are no special requirements for soldering components to board.
|
||||||
|
All chips and connectors can be soldered with standard manual soldering iron, although hot air station is recommended.
|
||||||
|
Interactive BOM has every component and its value highlighted on PCB drawings and it's strongly recommended to use during assembly process.
|
||||||
|
You can skip this step if PCB assembly service was used in previous steps.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -59,17 +63,18 @@ There are no special requirements for soldering components to board. All chips a
|
|||||||
|
|
||||||
**Please read the following instructions carefully before proceeding with programming.**
|
**Please read the following instructions carefully before proceeding with programming.**
|
||||||
|
|
||||||
For initial programming you are going to need a PC and a USB to UART (serial) adapter (3.3V signaling is required). These steps assume you are using modern Windows OS (version 10 or higher).
|
For initial programming you are going to need a PC and a USB to UART (serial) adapter (3.3V signaling is required).
|
||||||
|
These steps assume you are using modern Windows OS (version 10 or higher).
|
||||||
|
|
||||||
As for software here's list of required applications:
|
As for software here's list of required applications:
|
||||||
- [FT_PROG](https://ftdichip.com/utilities/#ft_prog) - FTDI FT232H EEPROM programming software
|
- [FT_PROG](https://ftdichip.com/utilities/#ft_prog) - FTDI FT232H EEPROM programming software
|
||||||
- [Python 3](https://www.python.org/downloads/) with `pip3` - necessary for initial programming script, `primer.py`
|
- [Python 3](https://www.python.org/downloads/) with `pip3` - necessary for initial programming script: `primer.py`
|
||||||
|
|
||||||
Programming must be done in specific order for `primer.py` script to work correctly.
|
Programming must be done in specific order for `primer.py` script to work correctly.
|
||||||
|
|
||||||
First, program FT232H EEPROM:
|
First, program FT232H EEPROM:
|
||||||
1. Connect SC64 board to the PC with USB-C cable
|
1. Connect SC64 board to the PC with USB-C cable
|
||||||
2. Locate FT232H EEPROM template in `fw/ftdi` folder
|
2. Locate FT232H EEPROM template `ft232h_config.xml`
|
||||||
3. Launch `FT_PROG` software
|
3. Launch `FT_PROG` software
|
||||||
4. Click on `Scan and parse` if no device has shown up
|
4. Click on `Scan and parse` if no device has shown up
|
||||||
5. Right click on SC64 device and choose `Apply Template -> From File`
|
5. Right click on SC64 device and choose `Apply Template -> From File`
|
||||||
@ -78,18 +83,18 @@ First, program FT232H EEPROM:
|
|||||||
|
|
||||||
Your SC64 should be ready for next programming step.
|
Your SC64 should be ready for next programming step.
|
||||||
|
|
||||||
Second, program FPGA, microcontroller and Flash memory:
|
Second, program FPGA, microcontroller and bootloader:
|
||||||
1. Disconnect SC64 board from power (unplug USB-C cable)
|
1. Disconnect SC64 board from power (unplug USB-C cable)
|
||||||
2. Connect serial adapter to `TX/RX/GND` pads marked on the PCB
|
2. Connect serial adapter to `TX/RX/GND` pads marked on the PCB
|
||||||
3. Connect serial adapter to the PC
|
3. Connect serial adapter to the PC
|
||||||
4. Check in device manager which port number `COMx` is assigned to serial adapter
|
4. Check in device manager which port number `COMx` is assigned to serial adapter
|
||||||
5. Connect SC64 board to the PC with USB-C cable (***IMPORTANT:*** connect it to the same computer as serial adapter)
|
5. Connect SC64 board to the PC with USB-C cable (***IMPORTANT:*** connect it to the same computer as serial adapter)
|
||||||
6. Locate `primer.py` script in root folder
|
6. Locate `primer.py` script in root folder
|
||||||
7. Make sure these files are located in the same folder as `primer.py` script: `requirements.txt`, `sc64.py`, `sc64-firmware.bin`
|
7. Make sure these files are located in the same folder as `primer.py` script: `requirements.txt`, `sc64-firmware-{version}.bin`
|
||||||
8. Run `pip3 install -r requirements.txt` to install required python packages
|
8. Run `pip3 install -r requirements.txt` to install required python packages
|
||||||
9. Run `python3 primer.py COMx sc64-firmware.bin` (replace `COMx` with port located in step **4**)
|
9. Run `python3 primer.py COMx sc64-firmware-{version}.bin` (replace `COMx` with port located in step **4**)
|
||||||
10. Follow the instructions on the screen
|
10. Follow the instructions on the screen
|
||||||
11. Wait until programming process has finished (**DO NOT STOP PROGRAMMING PROCESS OR DISCONNECT SC64 BOARD FROM PC**, doing so might irrecoverably break programming through UART header and you would need to program microcontroller, FPGA and bootloader with separate dedicated programming interfaces through *Tag-Connect* connector on the PCB)
|
11. Wait until programming process has finished (**DO NOT STOP PROGRAMMING PROCESS OR DISCONNECT SC64 BOARD FROM PC**, doing so might irrecoverably break programming through UART header and you would need to program FPGA and/or microcontroller with separate dedicated programming interfaces through *Tag-Connect* connector on the PCB)
|
||||||
|
|
||||||
Congratulations! Your SC64 flashcart should be ready for use!
|
Congratulations! Your SC64 flashcart should be ready for use!
|
||||||
|
|
||||||
@ -99,4 +104,7 @@ Congratulations! Your SC64 flashcart should be ready for use!
|
|||||||
|
|
||||||
*`primer.py` threw error on `Bootloader -> SC64 FLASH` step*
|
*`primer.py` threw error on `Bootloader -> SC64 FLASH` step*
|
||||||
|
|
||||||
This issue can be attributed to incorrectly programmed FT232H EEPROM in the first programming step. Check again in `FT_PROG` program if device was configured properly. Once FPGA and microcontroller has been programmed successfully `primer.py` script needs to be run in special mode. Please use command `python3 primer.py COMx sc64-firmware.bin --only-bootloader` to try programming bootloader again.
|
This issue can be attributed to incorrectly programmed FT232H EEPROM in the first programming step.
|
||||||
|
Check again in `FT_PROG` application if device was configured properly.
|
||||||
|
Once FPGA and microcontroller has been programmed successfully `primer.py` script needs to be run in special mode.
|
||||||
|
Please use command `python3 primer.py COMx sc64-firmware-{version}.bin --bootloader-only` to try programming bootloader again.
|
||||||
|
@ -47,20 +47,25 @@ void exception_fatal_handler (uint32_t exception_code, uint32_t interrupt_mask,
|
|||||||
|
|
||||||
display_init((uint32_t *) (&assets_sc64_logo_640_240_dimmed));
|
display_init((uint32_t *) (&assets_sc64_logo_640_240_dimmed));
|
||||||
|
|
||||||
|
display_printf("[ SC64 bootloader metadata ]\n");
|
||||||
display_printf("branch: %s | tag: %s\n", version->git_branch, version->git_tag);
|
display_printf("branch: %s | tag: %s\n", version->git_branch, version->git_tag);
|
||||||
display_printf("sha: %s\n", version->git_sha);
|
display_printf("sha: %s\n", version->git_sha);
|
||||||
display_printf("%s\n\n", version->git_message);
|
display_printf("msg: %s\n\n", version->git_message);
|
||||||
|
|
||||||
display_printf("%s\n", exception_get_description(exception_code));
|
if (exception_code != EXCEPTION_SYSCALL) {
|
||||||
display_printf(" pc: 0x%08lX sr: 0x%08lX cr: 0x%08lX va: 0x%08lX\n", e->epc.u32, e->sr, e->cr, e->badvaddr.u32);
|
display_printf("%s\n", exception_get_description(exception_code));
|
||||||
display_printf(" zr: 0x%08lX at: 0x%08lX v0: 0x%08lX v1: 0x%08lX\n", e->zr.u32, e->at.u32, e->v0.u32, e->v1.u32);
|
display_printf(" pc: 0x%08lX sr: 0x%08lX cr: 0x%08lX va: 0x%08lX\n", e->epc.u32, e->sr, e->cr, e->badvaddr.u32);
|
||||||
display_printf(" a0: 0x%08lX a1: 0x%08lX a2: 0x%08lX a3: 0x%08lX\n", e->a0.u32, e->a1.u32, e->a2.u32, e->a3.u32);
|
display_printf(" zr: 0x%08lX at: 0x%08lX v0: 0x%08lX v1: 0x%08lX\n", e->zr.u32, e->at.u32, e->v0.u32, e->v1.u32);
|
||||||
display_printf(" t0: 0x%08lX t1: 0x%08lX t2: 0x%08lX t3: 0x%08lX\n", e->t0.u32, e->t1.u32, e->t2.u32, e->t3.u32);
|
display_printf(" a0: 0x%08lX a1: 0x%08lX a2: 0x%08lX a3: 0x%08lX\n", e->a0.u32, e->a1.u32, e->a2.u32, e->a3.u32);
|
||||||
display_printf(" t4: 0x%08lX t5: 0x%08lX t6: 0x%08lX t7: 0x%08lX\n", e->t4.u32, e->t5.u32, e->t6.u32, e->t7.u32);
|
display_printf(" t0: 0x%08lX t1: 0x%08lX t2: 0x%08lX t3: 0x%08lX\n", e->t0.u32, e->t1.u32, e->t2.u32, e->t3.u32);
|
||||||
display_printf(" s0: 0x%08lX s1: 0x%08lX s2: 0x%08lX s3: 0x%08lX\n", e->s0.u32, e->s1.u32, e->s2.u32, e->s3.u32);
|
display_printf(" t4: 0x%08lX t5: 0x%08lX t6: 0x%08lX t7: 0x%08lX\n", e->t4.u32, e->t5.u32, e->t6.u32, e->t7.u32);
|
||||||
display_printf(" s4: 0x%08lX s5: 0x%08lX s6: 0x%08lX s7: 0x%08lX\n", e->s4.u32, e->s5.u32, e->s6.u32, e->s7.u32);
|
display_printf(" s0: 0x%08lX s1: 0x%08lX s2: 0x%08lX s3: 0x%08lX\n", e->s0.u32, e->s1.u32, e->s2.u32, e->s3.u32);
|
||||||
display_printf(" t8: 0x%08lX t9: 0x%08lX k0: 0x%08lX k1: 0x%08lX\n", e->t8.u32, e->t9.u32, e->k0.u32, e->k1.u32);
|
display_printf(" s4: 0x%08lX s5: 0x%08lX s6: 0x%08lX s7: 0x%08lX\n", e->s4.u32, e->s5.u32, e->s6.u32, e->s7.u32);
|
||||||
display_printf(" gp: 0x%08lX sp: 0x%08lX s8: 0x%08lX ra: 0x%08lX\n\n", e->gp.u32, e->sp.u32, e->s8.u32, e->ra.u32);
|
display_printf(" t8: 0x%08lX t9: 0x%08lX k0: 0x%08lX k1: 0x%08lX\n", e->t8.u32, e->t9.u32, e->k0.u32, e->k1.u32);
|
||||||
|
display_printf(" gp: 0x%08lX sp: 0x%08lX s8: 0x%08lX ra: 0x%08lX\n\n", e->gp.u32, e->sp.u32, e->s8.u32, e->ra.u32);
|
||||||
|
} else {
|
||||||
|
display_printf("[ Runtime error ]\n");
|
||||||
|
}
|
||||||
|
|
||||||
if (exception_code == EXCEPTION_INTERRUPT) {
|
if (exception_code == EXCEPTION_INTERRUPT) {
|
||||||
if (interrupt_mask & INTERRUPT_MASK_TIMER) {
|
if (interrupt_mask & INTERRUPT_MASK_TIMER) {
|
||||||
|
@ -41,7 +41,7 @@ static const char *fatfs_error_codes[] = {
|
|||||||
#define FF_CHECK(x, message, ...) { \
|
#define FF_CHECK(x, message, ...) { \
|
||||||
fresult = x; \
|
fresult = x; \
|
||||||
if (fresult != FR_OK) { \
|
if (fresult != FR_OK) { \
|
||||||
error_display(message " [%s]:\n %s\n", __VA_ARGS__ __VA_OPT__(,) #x, fatfs_error_codes[fresult]); \
|
error_display(message ".\nReason: %s", __VA_ARGS__ __VA_OPT__(,) fatfs_error_codes[fresult]); \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +55,11 @@ static void menu_check_load_address (void *address, size_t size) {
|
|||||||
void *bootloader_end = (void *) (&__bootloader_end);
|
void *bootloader_end = (void *) (&__bootloader_end);
|
||||||
|
|
||||||
if ((menu_start < usable_ram_start) || (menu_end > usable_ram_end)) {
|
if ((menu_start < usable_ram_start) || (menu_end > usable_ram_end)) {
|
||||||
error_display("Incorrect menu load address/size:\n Outside of usable RAM space\n");
|
error_display("Incorrect menu load address/size.\nReason: Outside of usable RAM space\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((menu_start < bootloader_end) && (bootloader_start < menu_end)) {
|
if ((menu_start < bootloader_end) && (bootloader_start < menu_end)) {
|
||||||
error_display("Incorrect menu load address/size:\n Overlapping bootloader space\n");
|
error_display("Incorrect menu load address/size.\nReason: Overlapping bootloader space\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,21 +72,21 @@ void menu_load_and_run (void) {
|
|||||||
UINT br;
|
UINT br;
|
||||||
size_t size = ROM_MAX_LOAD_SIZE;
|
size_t size = ROM_MAX_LOAD_SIZE;
|
||||||
|
|
||||||
FF_CHECK(f_mount(&fs, "", 1), "Couldn't mount drive");
|
FF_CHECK(f_mount(&fs, "", 1), "SD card initialize error. No SD card or invalid partition table");
|
||||||
FF_CHECK(f_open(&fil, "sc64menu.n64", FA_READ), "Couldn't open menu file");
|
FF_CHECK(f_open(&fil, "sc64menu.n64", FA_READ), "Could not open menu executable (sc64menu.n64)");
|
||||||
FF_CHECK(f_lseek(&fil, ROM_ENTRY_OFFSET), "Couldn't seek to entry point offset");
|
FF_CHECK(f_lseek(&fil, ROM_ENTRY_OFFSET), "Could not seek to entry point offset");
|
||||||
FF_CHECK(f_read(&fil, &menu, sizeof(menu), &br), "Couldn't read entry point");
|
FF_CHECK(f_read(&fil, &menu, sizeof(menu), &br), "Could not read entry point");
|
||||||
FF_CHECK(f_lseek(&fil, ROM_CODE_OFFSET), "Couldn't seek to code start offset");
|
FF_CHECK(f_lseek(&fil, ROM_CODE_OFFSET), "Could not seek to code start offset");
|
||||||
if ((f_size(&fil) - ROM_CODE_OFFSET) < size) {
|
if ((f_size(&fil) - ROM_CODE_OFFSET) < size) {
|
||||||
size = (size_t) (f_size(&fil) - ROM_CODE_OFFSET);
|
size = (size_t) (f_size(&fil) - ROM_CODE_OFFSET);
|
||||||
}
|
}
|
||||||
menu_check_load_address(menu, size);
|
menu_check_load_address(menu, size);
|
||||||
cache_data_hit_writeback_invalidate(menu, size);
|
cache_data_hit_writeback_invalidate(menu, size);
|
||||||
cache_inst_hit_invalidate(menu, size);
|
cache_inst_hit_invalidate(menu, size);
|
||||||
FF_CHECK(f_read(&fil, menu, size, &br), "Couldn't read menu file");
|
FF_CHECK(f_read(&fil, menu, size, &br), "Could not read menu file");
|
||||||
FF_CHECK((br != size) ? FR_INT_ERR : FR_OK, "Read size is different than expected");
|
FF_CHECK((br != size) ? FR_INT_ERR : FR_OK, "Read size is different than expected");
|
||||||
FF_CHECK(f_close(&fil), "Couldn't close menu file");
|
FF_CHECK(f_close(&fil), "Could not close menu file");
|
||||||
FF_CHECK(f_unmount(""), "Couldn't unmount drive");
|
FF_CHECK(f_unmount(""), "Could not unmount drive");
|
||||||
|
|
||||||
deinit();
|
deinit();
|
||||||
|
|
||||||
|
@ -397,10 +397,8 @@ bool cfg_update_setting (uint32_t *args) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool cfg_set_rom_write_enable (bool value) {
|
void cfg_set_rom_write_enable (bool value) {
|
||||||
uint32_t scr = fpga_reg_get(REG_CFG_SCR);
|
|
||||||
cfg_change_scr_bits(CFG_SCR_ROM_WRITE_ENABLED, value);
|
cfg_change_scr_bits(CFG_SCR_ROM_WRITE_ENABLED, value);
|
||||||
return (scr & CFG_SCR_ROM_WRITE_ENABLED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save_type_t cfg_get_save_type (void) {
|
save_type_t cfg_get_save_type (void) {
|
||||||
@ -472,7 +470,7 @@ void cfg_process (void) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'V':
|
case 'V':
|
||||||
args[0] = version_firmware();
|
version_firmware(&args[0], &args[1]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'c':
|
case 'c':
|
||||||
|
@ -21,7 +21,7 @@ bool cfg_query (uint32_t *args);
|
|||||||
bool cfg_update (uint32_t *args);
|
bool cfg_update (uint32_t *args);
|
||||||
bool cfg_query_setting (uint32_t *args);
|
bool cfg_query_setting (uint32_t *args);
|
||||||
bool cfg_update_setting (uint32_t *args);
|
bool cfg_update_setting (uint32_t *args);
|
||||||
bool cfg_set_rom_write_enable (bool value);
|
void cfg_set_rom_write_enable (bool value);
|
||||||
save_type_t cfg_get_save_type (void);
|
save_type_t cfg_get_save_type (void);
|
||||||
void cfg_get_time (uint32_t *args);
|
void cfg_get_time (uint32_t *args);
|
||||||
void cfg_set_time (uint32_t *args);
|
void cfg_set_time (uint32_t *args);
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
|
|
||||||
bool flash_program (uint32_t src, uint32_t dst, uint32_t length) {
|
bool flash_program (uint32_t src, uint32_t dst, uint32_t length) {
|
||||||
if (((src + length) >= FLASH_ADDRESS) && (src < (FLASH_ADDRESS + FLASH_SIZE))) {
|
if (((src + length) > FLASH_ADDRESS) && (src < (FLASH_ADDRESS + FLASH_SIZE))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((dst < FLASH_ADDRESS) || ((dst + length) >= (FLASH_ADDRESS + FLASH_SIZE))) {
|
if ((dst < FLASH_ADDRESS) || ((dst + length) > (FLASH_ADDRESS + FLASH_SIZE))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
while (length > 0) {
|
while (length > 0) {
|
||||||
@ -29,15 +29,18 @@ void flash_wait_busy (void) {
|
|||||||
fpga_mem_read(FLASH_ADDRESS, 2, dummy);
|
fpga_mem_read(FLASH_ADDRESS, 2, dummy);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool flash_erase_block (uint32_t offset) {
|
bool flash_erase_block (uint32_t address) {
|
||||||
if ((offset % FLASH_ERASE_BLOCK_SIZE) != 0) {
|
if ((address % FLASH_ERASE_BLOCK_SIZE) != 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
offset &= (FLASH_SIZE - 1);
|
if ((address < FLASH_ADDRESS) || (address >= (FLASH_ADDRESS + FLASH_SIZE))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
address &= (FLASH_SIZE - 1);
|
||||||
for (int i = 0; i < (FLASH_ERASE_BLOCK_SIZE / ERASE_BLOCK_SIZE); i++) {
|
for (int i = 0; i < (FLASH_ERASE_BLOCK_SIZE / ERASE_BLOCK_SIZE); i++) {
|
||||||
fpga_reg_set(REG_FLASH_SCR, offset);
|
fpga_reg_set(REG_FLASH_SCR, address);
|
||||||
while (fpga_reg_get(REG_FLASH_SCR) & FLASH_SCR_BUSY);
|
while (fpga_reg_get(REG_FLASH_SCR) & FLASH_SCR_BUSY);
|
||||||
offset += ERASE_BLOCK_SIZE;
|
address += ERASE_BLOCK_SIZE;
|
||||||
}
|
}
|
||||||
flash_wait_busy();
|
flash_wait_busy();
|
||||||
return false;
|
return false;
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
bool flash_program (uint32_t src, uint32_t dst, uint32_t length);
|
bool flash_program (uint32_t src, uint32_t dst, uint32_t length);
|
||||||
void flash_wait_busy (void);
|
void flash_wait_busy (void);
|
||||||
bool flash_erase_block (uint32_t offset);
|
bool flash_erase_block (uint32_t address);
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
#include "vendor.h"
|
#include "vendor.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define SDRAM_ADDRESS (0x00000000UL)
|
||||||
|
#define SDRAM_LENGTH (64 * 1024 * 1024)
|
||||||
|
|
||||||
#define UPDATE_MAGIC_START (0x54535055UL)
|
#define UPDATE_MAGIC_START (0x54535055UL)
|
||||||
#define BOOTLOADER_ADDRESS (0x04E00000UL)
|
#define BOOTLOADER_ADDRESS (0x04E00000UL)
|
||||||
#define BOOTLOADER_LENGTH (0x001E0000UL)
|
#define BOOTLOADER_LENGTH (0x001E0000UL)
|
||||||
@ -191,6 +194,10 @@ update_error_t update_backup (uint32_t address, uint32_t *length) {
|
|||||||
uint32_t fpga_length;
|
uint32_t fpga_length;
|
||||||
uint32_t bootloader_length;
|
uint32_t bootloader_length;
|
||||||
|
|
||||||
|
if (address >= (SDRAM_ADDRESS + SDRAM_LENGTH)) {
|
||||||
|
return UPDATE_ERROR_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
*length = update_write_token(&address);
|
*length = update_write_token(&address);
|
||||||
|
|
||||||
*length += update_prepare_chunk(&address, CHUNK_ID_MCU_DATA);
|
*length += update_prepare_chunk(&address, CHUNK_ID_MCU_DATA);
|
||||||
@ -214,6 +221,10 @@ update_error_t update_backup (uint32_t address, uint32_t *length) {
|
|||||||
}
|
}
|
||||||
*length += update_finalize_chunk(&address, bootloader_length);
|
*length += update_finalize_chunk(&address, bootloader_length);
|
||||||
|
|
||||||
|
if ((address + *length) > (SDRAM_ADDRESS + SDRAM_LENGTH)) {
|
||||||
|
return UPDATE_ERROR_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
return UPDATE_OK;
|
return UPDATE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +234,13 @@ update_error_t update_prepare (uint32_t address, uint32_t length) {
|
|||||||
uint32_t data_address;
|
uint32_t data_address;
|
||||||
uint32_t data_length;
|
uint32_t data_length;
|
||||||
|
|
||||||
|
if ((address >= (SDRAM_ADDRESS + SDRAM_LENGTH)) || (length > SDRAM_LENGTH)) {
|
||||||
|
return UPDATE_ERROR_ADDRESS;
|
||||||
|
}
|
||||||
|
if (end_address > (SDRAM_ADDRESS + SDRAM_LENGTH)) {
|
||||||
|
return UPDATE_ERROR_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
if (update_check_token(&address)) {
|
if (update_check_token(&address)) {
|
||||||
return UPDATE_ERROR_TOKEN;
|
return UPDATE_ERROR_TOKEN;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ typedef enum {
|
|||||||
UPDATE_ERROR_SIZE,
|
UPDATE_ERROR_SIZE,
|
||||||
UPDATE_ERROR_UNKNOWN_CHUNK,
|
UPDATE_ERROR_UNKNOWN_CHUNK,
|
||||||
UPDATE_ERROR_READ,
|
UPDATE_ERROR_READ,
|
||||||
|
UPDATE_ERROR_ADDRESS,
|
||||||
} update_error_t;
|
} update_error_t;
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,10 +10,20 @@
|
|||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define BOOTLOADER_ADDRESS (0x04E00000UL)
|
||||||
|
#define BOOTLOADER_LENGTH (1920 * 1024)
|
||||||
|
|
||||||
|
#define MEMORY_LENGTH (0x05002980UL)
|
||||||
|
|
||||||
|
#define RX_FLUSH_ADDRESS (0x07F00000UL)
|
||||||
|
#define RX_FLUSH_LENGTH (1 * 1024 * 1024)
|
||||||
|
|
||||||
|
|
||||||
enum rx_state {
|
enum rx_state {
|
||||||
RX_STATE_IDLE,
|
RX_STATE_IDLE,
|
||||||
RX_STATE_ARGS,
|
RX_STATE_ARGS,
|
||||||
RX_STATE_DATA,
|
RX_STATE_DATA,
|
||||||
|
RX_STATE_FLUSH,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum tx_state {
|
enum tx_state {
|
||||||
@ -129,6 +139,21 @@ static bool usb_rx_cmd (uint8_t *cmd) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool usb_validate_address_length (uint32_t address, uint32_t length, bool exclude_bootloader) {
|
||||||
|
if ((address >= MEMORY_LENGTH) || (length > MEMORY_LENGTH)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((address + length) > MEMORY_LENGTH) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (exclude_bootloader) {
|
||||||
|
if (((address + length) > BOOTLOADER_ADDRESS) && (address < (BOOTLOADER_ADDRESS + BOOTLOADER_LENGTH))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static void usb_rx_process (void) {
|
static void usb_rx_process (void) {
|
||||||
if (p.rx_state == RX_STATE_IDLE) {
|
if (p.rx_state == RX_STATE_IDLE) {
|
||||||
if (!p.response_pending && usb_rx_cmd(&p.rx_cmd)) {
|
if (!p.response_pending && usb_rx_cmd(&p.rx_cmd)) {
|
||||||
@ -166,8 +191,8 @@ static void usb_rx_process (void) {
|
|||||||
case 'V':
|
case 'V':
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
p.response_info.data_length = 4;
|
p.response_info.data_length = 8;
|
||||||
p.response_info.data[0] = version_firmware();
|
version_firmware(&p.response_info.data[0], &p.response_info.data[1]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'R':
|
case 'R':
|
||||||
@ -229,17 +254,25 @@ static void usb_rx_process (void) {
|
|||||||
case 'm':
|
case 'm':
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
p.response_info.dma_address = p.rx_args[0];
|
if (usb_validate_address_length(p.rx_args[0], p.rx_args[1], false)) {
|
||||||
p.response_info.dma_length = p.rx_args[1];
|
p.response_error = true;
|
||||||
|
} else {
|
||||||
|
p.response_info.dma_address = p.rx_args[0];
|
||||||
|
p.response_info.dma_length = p.rx_args[1];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'M':
|
case 'M':
|
||||||
if (usb_dma_ready()) {
|
if (usb_dma_ready()) {
|
||||||
if (!p.rx_dma_running) {
|
if (!p.rx_dma_running) {
|
||||||
fpga_reg_set(REG_USB_DMA_ADDRESS, p.rx_args[0]);
|
if (usb_validate_address_length(p.rx_args[0], p.rx_args[1], true)) {
|
||||||
fpga_reg_set(REG_USB_DMA_LENGTH, p.rx_args[1]);
|
p.rx_state = RX_STATE_FLUSH;
|
||||||
fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START);
|
} else {
|
||||||
p.rx_dma_running = true;
|
fpga_reg_set(REG_USB_DMA_ADDRESS, p.rx_args[0]);
|
||||||
|
fpga_reg_set(REG_USB_DMA_LENGTH, p.rx_args[1]);
|
||||||
|
fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START);
|
||||||
|
p.rx_dma_running = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
@ -248,7 +281,9 @@ static void usb_rx_process (void) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'U':
|
case 'U':
|
||||||
if ((p.read_length > 0) && usb_dma_ready()) {
|
if (p.rx_args[1] == 0) {
|
||||||
|
p.rx_state = RX_STATE_IDLE;
|
||||||
|
} else if ((p.read_length > 0) && usb_dma_ready()) {
|
||||||
uint32_t length = (p.read_length > p.rx_args[1]) ? p.rx_args[1] : p.read_length;
|
uint32_t length = (p.read_length > p.rx_args[1]) ? p.rx_args[1] : p.read_length;
|
||||||
if (!p.rx_dma_running) {
|
if (!p.rx_dma_running) {
|
||||||
fpga_reg_set(REG_USB_DMA_ADDRESS, p.read_address);
|
fpga_reg_set(REG_USB_DMA_ADDRESS, p.read_address);
|
||||||
@ -258,12 +293,10 @@ static void usb_rx_process (void) {
|
|||||||
p.read_ready = false;
|
p.read_ready = false;
|
||||||
} else {
|
} else {
|
||||||
p.rx_args[1] -= length;
|
p.rx_args[1] -= length;
|
||||||
|
p.rx_dma_running = false;
|
||||||
p.read_length -= length;
|
p.read_length -= length;
|
||||||
p.read_address += length;
|
p.read_address += length;
|
||||||
p.read_ready = true;
|
p.read_ready = true;
|
||||||
if (p.rx_args[1] == 0) {
|
|
||||||
p.rx_state = RX_STATE_IDLE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -285,24 +318,26 @@ static void usb_rx_process (void) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'P':
|
case 'P':
|
||||||
p.response_error = flash_erase_block(p.rx_args[0]);
|
if (usb_validate_address_length(p.rx_args[0], FLASH_ERASE_BLOCK_SIZE, true)) {
|
||||||
|
p.response_error = true;
|
||||||
|
} else {
|
||||||
|
p.response_error = flash_erase_block(p.rx_args[0]);
|
||||||
|
}
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'f': {
|
case 'f':
|
||||||
bool rom_write_enable_restore = cfg_set_rom_write_enable(false);
|
cfg_set_rom_write_enable(false);
|
||||||
p.response_info.data[0] = update_backup(p.rx_args[0], &p.response_info.data[1]);
|
p.response_info.data[0] = update_backup(p.rx_args[0], &p.response_info.data[1]);
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
p.response_error = (p.response_info.data[0] != UPDATE_OK);
|
p.response_error = (p.response_info.data[0] != UPDATE_OK);
|
||||||
p.response_info.data_length = 8;
|
p.response_info.data_length = 8;
|
||||||
cfg_set_rom_write_enable(rom_write_enable_restore);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case 'F': {
|
case 'F':
|
||||||
bool rom_write_enable_restore = cfg_set_rom_write_enable(false);
|
cfg_set_rom_write_enable(false);
|
||||||
p.response_info.data[0] = update_prepare(p.rx_args[0], p.rx_args[1]);
|
p.response_info.data[0] = update_prepare(p.rx_args[0], p.rx_args[1]);
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
@ -311,10 +346,8 @@ static void usb_rx_process (void) {
|
|||||||
p.response_info.done_callback = update_start;
|
p.response_info.done_callback = update_start;
|
||||||
} else {
|
} else {
|
||||||
p.response_error = true;
|
p.response_error = true;
|
||||||
cfg_set_rom_write_enable(rom_write_enable_restore);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case '?':
|
case '?':
|
||||||
p.rx_state = RX_STATE_IDLE;
|
p.rx_state = RX_STATE_IDLE;
|
||||||
@ -336,10 +369,26 @@ static void usb_rx_process (void) {
|
|||||||
p.response_pending = true;
|
p.response_pending = true;
|
||||||
p.response_error = true;
|
p.response_error = true;
|
||||||
p.response_info.data_length = 4;
|
p.response_info.data_length = 4;
|
||||||
p.response_info.data[0] = 0xFF;
|
p.response_info.data[0] = 0xFFFFFFFF;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p.rx_state == RX_STATE_FLUSH) {
|
||||||
|
if (usb_dma_ready()) {
|
||||||
|
if (p.rx_args[1] != 0) {
|
||||||
|
uint32_t length = (p.rx_args[1] > RX_FLUSH_LENGTH) ? RX_FLUSH_LENGTH : p.rx_args[1];
|
||||||
|
fpga_reg_set(REG_USB_DMA_ADDRESS, RX_FLUSH_ADDRESS);
|
||||||
|
fpga_reg_set(REG_USB_DMA_LENGTH, length);
|
||||||
|
fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START);
|
||||||
|
p.rx_args[1] -= length;
|
||||||
|
} else {
|
||||||
|
p.rx_state = RX_STATE_IDLE;
|
||||||
|
p.response_pending = true;
|
||||||
|
p.response_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void usb_tx_process (void) {
|
static void usb_tx_process (void) {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
|
|
||||||
#define VERSION_MAJOR (2)
|
#define VERSION_MAJOR (2)
|
||||||
#define VERSION_MINOR (12)
|
#define VERSION_MINOR (12)
|
||||||
|
#define VERSION_REVISION (1)
|
||||||
|
|
||||||
|
|
||||||
uint32_t version_firmware (void) {
|
void version_firmware (uint32_t *version, uint32_t *revision) {
|
||||||
return ((VERSION_MAJOR << 16) | (VERSION_MINOR));
|
*version = ((VERSION_MAJOR << 16) | (VERSION_MINOR));
|
||||||
|
*revision = VERSION_REVISION;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
|
||||||
uint32_t version_firmware (void);
|
void version_firmware (uint32_t *version, uint32_t *revision);
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
9
sw/deployer/.gitignore
vendored
Normal file
9
sw/deployer/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/target
|
||||||
|
*.bin
|
||||||
|
*.eep
|
||||||
|
*.fla
|
||||||
|
*.n64
|
||||||
|
*.png
|
||||||
|
*.srm
|
||||||
|
*.v64
|
||||||
|
*.z64
|
1335
sw/deployer/Cargo.lock
generated
Normal file
1335
sw/deployer/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
sw/deployer/Cargo.toml
Normal file
27
sw/deployer/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "sc64deployer"
|
||||||
|
version = "2.12.1"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Polprzewodnikowy"]
|
||||||
|
description = "SC64 loader and control software"
|
||||||
|
documentation = "https://github.com/Polprzewodnikowy/SummerCart64"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.23"
|
||||||
|
clap = { version = "4.1.6", features = ["derive"] }
|
||||||
|
clap-num = "1.0.2"
|
||||||
|
colored = "2.0.0"
|
||||||
|
crc32fast = "1.3.2"
|
||||||
|
ctrlc = "3.2.5"
|
||||||
|
encoding_rs = "0.8.32"
|
||||||
|
hex = "0.4.3"
|
||||||
|
image = "0.24.5"
|
||||||
|
include-flate = { version = "0.2.0", features = ["stable"] }
|
||||||
|
md5 = "0.7.0"
|
||||||
|
panic-message = "0.3.0"
|
||||||
|
rust-ini = "0.18.0"
|
||||||
|
serialport = { git = "https://github.com/serialport/serialport-rs", branch = "main" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
strip = true
|
18178
sw/deployer/data/mupen64plus.ini
vendored
Normal file
18178
sw/deployer/data/mupen64plus.ini
vendored
Normal file
File diff suppressed because it is too large
Load Diff
396
sw/deployer/src/debug.rs
Normal file
396
sw/deployer/src/debug.rs
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
use crate::sc64;
|
||||||
|
use chrono::Local;
|
||||||
|
use colored::Colorize;
|
||||||
|
use panic_message::panic_message;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{stdin, ErrorKind, Read, Write},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
panic,
|
||||||
|
sync::mpsc::{channel, Receiver, Sender},
|
||||||
|
thread::{sleep, spawn},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Handler {
|
||||||
|
header: Option<Vec<u8>>,
|
||||||
|
line_rx: Receiver<String>,
|
||||||
|
gdb_tx: Sender<Vec<u8>>,
|
||||||
|
gdb_rx: Receiver<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DataType {
|
||||||
|
Text,
|
||||||
|
RawBinary,
|
||||||
|
Header,
|
||||||
|
Screenshot,
|
||||||
|
GDB,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for DataType {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0x01 => Self::Text,
|
||||||
|
0x02 => Self::RawBinary,
|
||||||
|
0x03 => Self::Header,
|
||||||
|
0x04 => Self::Screenshot,
|
||||||
|
0xDB => Self::GDB,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DataType> for u8 {
|
||||||
|
fn from(value: DataType) -> Self {
|
||||||
|
match value {
|
||||||
|
DataType::Text => 0x01,
|
||||||
|
DataType::RawBinary => 0x02,
|
||||||
|
DataType::Header => 0x03,
|
||||||
|
DataType::Screenshot => 0x04,
|
||||||
|
DataType::GDB => 0xDB,
|
||||||
|
DataType::Unknown => 0xFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DataType> for u32 {
|
||||||
|
fn from(value: DataType) -> Self {
|
||||||
|
u8::from(value) as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum ScreenshotPixelFormat {
|
||||||
|
Rgba16,
|
||||||
|
Rgba32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for ScreenshotPixelFormat {
|
||||||
|
type Error = String;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
2 => Self::Rgba16,
|
||||||
|
4 => Self::Rgba32,
|
||||||
|
_ => return Err("Invalid pixel format for screenshot metadata".into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScreenshotPixelFormat> for u32 {
|
||||||
|
fn from(value: ScreenshotPixelFormat) -> Self {
|
||||||
|
match value {
|
||||||
|
ScreenshotPixelFormat::Rgba16 => 2,
|
||||||
|
ScreenshotPixelFormat::Rgba32 => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScreenshotMetadata {
|
||||||
|
format: ScreenshotPixelFormat,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for ScreenshotMetadata {
|
||||||
|
type Error = String;
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 16 {
|
||||||
|
return Err("Invalid header length for screenshot metadata".into());
|
||||||
|
}
|
||||||
|
if u32::from_be_bytes(value[0..4].try_into().unwrap()) != DataType::Screenshot.into() {
|
||||||
|
return Err("Invalid header datatype for screenshot metadata".into());
|
||||||
|
}
|
||||||
|
let format = u32::from_be_bytes(value[4..8].try_into().unwrap());
|
||||||
|
let width = u32::from_be_bytes(value[8..12].try_into().unwrap());
|
||||||
|
let height = u32::from_be_bytes(value[12..16].try_into().unwrap());
|
||||||
|
if width > 4096 || height > 4096 {
|
||||||
|
return Err("Invalid width or height for screenshot metadata".into());
|
||||||
|
}
|
||||||
|
Ok(ScreenshotMetadata {
|
||||||
|
format: format.try_into()?,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
pub fn process_user_input(&self) -> Option<sc64::DebugPacket> {
|
||||||
|
if let Ok(line) = self.line_rx.try_recv() {
|
||||||
|
if line.len() == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut data: Vec<u8> = Vec::new();
|
||||||
|
if line.matches("@").count() != 2 {
|
||||||
|
data.append(&mut line.as_bytes().to_vec());
|
||||||
|
data.append(&mut [b'\0'].to_vec());
|
||||||
|
return Some(sc64::DebugPacket {
|
||||||
|
datatype: DataType::Text.into(),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let start = line.find("@").unwrap();
|
||||||
|
let end = line.rfind("@").unwrap();
|
||||||
|
let path = &line[start + 1..end];
|
||||||
|
if path.len() == 0 {
|
||||||
|
println!("Invalid path provided");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut file = match File::open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(error) => {
|
||||||
|
println!("Couldn't open file: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let length = match file.metadata() {
|
||||||
|
Ok(metadata) => metadata.len(),
|
||||||
|
Err(error) => {
|
||||||
|
println!("Couldn't get file length: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut data = vec![0u8; length as usize];
|
||||||
|
if let Err(error) = file.read_exact(&mut data) {
|
||||||
|
println!("Couldn't read file contents: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if line.starts_with("@") && line.ends_with("@") {
|
||||||
|
return Some(sc64::DebugPacket {
|
||||||
|
datatype: DataType::RawBinary.into(),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let mut combined_data: Vec<u8> = Vec::new();
|
||||||
|
combined_data.append(&mut line[0..start].as_bytes().to_vec());
|
||||||
|
combined_data.append(&mut [b'@'].to_vec());
|
||||||
|
combined_data.append(&mut format!("{length}").into_bytes());
|
||||||
|
combined_data.append(&mut [b'@'].to_vec());
|
||||||
|
combined_data.append(&mut data);
|
||||||
|
combined_data.append(&mut [b'\0'].to_vec());
|
||||||
|
return Some(sc64::DebugPacket {
|
||||||
|
datatype: DataType::Text.into(),
|
||||||
|
data: combined_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_debug_packet(&mut self, debug_packet: sc64::DebugPacket) {
|
||||||
|
let sc64::DebugPacket { datatype, data } = debug_packet;
|
||||||
|
match datatype.into() {
|
||||||
|
DataType::Text => self.handle_datatype_text(&data),
|
||||||
|
DataType::RawBinary => self.handle_datatype_raw_binary(&data),
|
||||||
|
DataType::Header => self.handle_datatype_header(&data),
|
||||||
|
DataType::Screenshot => self.handle_datatype_screenshot(&data),
|
||||||
|
DataType::GDB => self.handle_datatype_gdb(&data),
|
||||||
|
_ => {
|
||||||
|
println!("Unknown debug packet datatype: 0x{datatype:02X}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_datatype_text(&self, data: &[u8]) {
|
||||||
|
print!("{}", String::from_utf8_lossy(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_datatype_raw_binary(&self, data: &[u8]) {
|
||||||
|
let filename = &self.generate_filename("binaryout", "bin");
|
||||||
|
match File::create(filename) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
if let Err(error) = file.write_all(data) {
|
||||||
|
println!("Error during raw binary save: {error}");
|
||||||
|
}
|
||||||
|
println!("Wrote {} bytes to [{}]", data.len(), filename);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
println!("Error during raw binary file creation: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_datatype_header(&mut self, data: &[u8]) {
|
||||||
|
self.header = Some(data.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_datatype_screenshot(&mut self, data: &[u8]) {
|
||||||
|
let header = match self.header.take() {
|
||||||
|
Some(header) => header,
|
||||||
|
None => {
|
||||||
|
println!("Got screenshot packet without header data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ScreenshotMetadata {
|
||||||
|
format,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
} = match header.try_into() {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(error) => {
|
||||||
|
println!("{error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let format_size: u32 = format.into();
|
||||||
|
if data.len() as u32 != format_size * width * height {
|
||||||
|
println!("Data length did not match header data for screenshot datatype");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut image = image::RgbaImage::new(width, height);
|
||||||
|
for (x, y, pixel) in image.enumerate_pixels_mut() {
|
||||||
|
let location = ((x + (y * width)) * format_size) as usize;
|
||||||
|
let p = &data[location..location + format_size as usize];
|
||||||
|
pixel.0 = match format {
|
||||||
|
ScreenshotPixelFormat::Rgba16 => {
|
||||||
|
let r = ((p[0] >> 3) & 0x1F) << 3;
|
||||||
|
let g = (((p[0] & 0x07) << 2) | ((p[1] >> 6) & 0x03)) << 3;
|
||||||
|
let b = ((p[1] >> 1) & 0x1F) << 3;
|
||||||
|
let a = ((p[1]) & 0x01) * 255;
|
||||||
|
[r, g, b, a]
|
||||||
|
}
|
||||||
|
ScreenshotPixelFormat::Rgba32 => [p[0], p[1], p[2], p[3]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let filename = &self.generate_filename("screenshot", "png");
|
||||||
|
if let Some(error) = image.save(filename).err() {
|
||||||
|
println!("Error during image save: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("Wrote {width}x{height} pixels to [{filename}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_datatype_gdb(&self, data: &[u8]) {
|
||||||
|
self.gdb_tx.send(data.to_vec()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_filename(&self, prefix: &str, extension: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{prefix}-{}.{extension}",
|
||||||
|
Local::now().format("%y%m%d%H%M%S.%f")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive_gdb_packet(&self) -> Option<sc64::DebugPacket> {
|
||||||
|
if let Some(data) = self.gdb_rx.try_recv().ok() {
|
||||||
|
Some(sc64::DebugPacket {
|
||||||
|
datatype: DataType::GDB.into(),
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(gdb_port: Option<u16>) -> Result<Handler, sc64::Error> {
|
||||||
|
let (line_tx, line_rx) = channel::<String>();
|
||||||
|
let (gdb_tx, gdb_loop_rx) = channel::<Vec<u8>>();
|
||||||
|
let (gdb_loop_tx, gdb_rx) = channel::<Vec<u8>>();
|
||||||
|
|
||||||
|
spawn(move || stdin_thread(line_tx));
|
||||||
|
|
||||||
|
if let Some(port) = gdb_port {
|
||||||
|
let listener = TcpListener::bind(format!("0.0.0.0:{port}"))
|
||||||
|
.map_err(|_| sc64::Error::new("Couldn't open GDB TCP socket port"))?;
|
||||||
|
listener.set_nonblocking(true).map_err(|_| {
|
||||||
|
sc64::Error::new("Couldn't set GDB TCP socket listener as non-blocking")
|
||||||
|
})?;
|
||||||
|
spawn(move || gdb_thread(listener, gdb_loop_tx, gdb_loop_rx));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Handler {
|
||||||
|
header: None,
|
||||||
|
line_rx,
|
||||||
|
gdb_tx,
|
||||||
|
gdb_rx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stdin_thread(line_tx: Sender<String>) {
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
if stdin().read_line(&mut line).is_ok() {
|
||||||
|
if line_tx.send(line).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gdb_thread(listener: TcpListener, gdb_tx: Sender<Vec<u8>>, gdb_rx: Receiver<Vec<u8>>) {
|
||||||
|
match panic::catch_unwind(|| gdb_loop(listener, gdb_tx, gdb_rx)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(payload) => {
|
||||||
|
eprintln!("{}", panic_message(&payload).red());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gdb_loop(listener: TcpListener, gdb_tx: Sender<Vec<u8>>, gdb_rx: Receiver<Vec<u8>>) {
|
||||||
|
for tcp_stream in listener.incoming() {
|
||||||
|
match tcp_stream {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
handle_gdb_connection(&mut stream, &gdb_tx, &gdb_rx);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() == ErrorKind::WouldBlock {
|
||||||
|
sleep(Duration::from_millis(1));
|
||||||
|
} else {
|
||||||
|
panic!("{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_gdb_connection(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
gdb_tx: &Sender<Vec<u8>>,
|
||||||
|
gdb_rx: &Receiver<Vec<u8>>,
|
||||||
|
) {
|
||||||
|
const GDB_DATA_BUFFER: usize = 64 * 1024;
|
||||||
|
|
||||||
|
let mut buffer = vec![0u8; GDB_DATA_BUFFER];
|
||||||
|
|
||||||
|
let peer = stream.peer_addr().unwrap();
|
||||||
|
|
||||||
|
println!("[GDB]: New connection ({peer})");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match stream.read(&mut buffer) {
|
||||||
|
Ok(length) => {
|
||||||
|
if length > 0 {
|
||||||
|
gdb_tx.send(buffer[0..length].to_vec()).ok();
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
println!("[GDB]: Connection closed ({peer})");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() != ErrorKind::WouldBlock {
|
||||||
|
println!("[GDB]: Connection closed ({peer}), read IO error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(data) = gdb_rx.try_recv() {
|
||||||
|
match stream.write_all(&data) {
|
||||||
|
Ok(()) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[GDB]: Connection closed ({peer}), write IO error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
}
|
306
sw/deployer/src/disk.rs
Normal file
306
sw/deployer/src/disk.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
use crate::sc64::Error;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Seek, SeekFrom, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLOCKS_PER_TRACK: usize = 2;
|
||||||
|
const SECTORS_PER_BLOCK: usize = 85;
|
||||||
|
const SYSTEM_SECTOR_LENGTH: usize = 232;
|
||||||
|
const BAD_TRACKS_PER_ZONE: usize = 12;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum Format {
|
||||||
|
Retail,
|
||||||
|
Development,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SystemAreaInfo<'a> {
|
||||||
|
format: Format,
|
||||||
|
sector_length: usize,
|
||||||
|
sys_lba: &'a [usize],
|
||||||
|
bad_lba: &'a [usize],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYSTEM_AREA: [SystemAreaInfo; 2] = [
|
||||||
|
SystemAreaInfo {
|
||||||
|
format: Format::Retail,
|
||||||
|
sector_length: 232,
|
||||||
|
sys_lba: &[9, 8, 1, 0],
|
||||||
|
bad_lba: &[2, 3, 10, 11, 12, 16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
|
},
|
||||||
|
SystemAreaInfo {
|
||||||
|
format: Format::Development,
|
||||||
|
sector_length: 192,
|
||||||
|
sys_lba: &[11, 10, 3, 2],
|
||||||
|
bad_lba: &[0, 1, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ID_LBAS: [usize; 2] = [15, 14];
|
||||||
|
|
||||||
|
struct DiskZone {
|
||||||
|
head: usize,
|
||||||
|
sector_length: usize,
|
||||||
|
tracks: usize,
|
||||||
|
track_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! zone {
|
||||||
|
($h:expr, $l:expr, $t:expr, $o:expr) => {
|
||||||
|
DiskZone {
|
||||||
|
head: $h,
|
||||||
|
sector_length: $l,
|
||||||
|
tracks: $t,
|
||||||
|
track_offset: $o,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZONE_MAPPING: [DiskZone; 16] = [
|
||||||
|
zone!(0, 232, 158, 0),
|
||||||
|
zone!(0, 216, 158, 158),
|
||||||
|
zone!(0, 208, 149, 316),
|
||||||
|
zone!(0, 192, 149, 465),
|
||||||
|
zone!(0, 176, 149, 614),
|
||||||
|
zone!(0, 160, 149, 763),
|
||||||
|
zone!(0, 144, 149, 912),
|
||||||
|
zone!(0, 128, 114, 1061),
|
||||||
|
zone!(1, 216, 158, 0),
|
||||||
|
zone!(1, 208, 158, 158),
|
||||||
|
zone!(1, 192, 149, 316),
|
||||||
|
zone!(1, 176, 149, 465),
|
||||||
|
zone!(1, 160, 149, 614),
|
||||||
|
zone!(1, 144, 149, 763),
|
||||||
|
zone!(1, 128, 149, 912),
|
||||||
|
zone!(1, 112, 114, 1061),
|
||||||
|
];
|
||||||
|
|
||||||
|
const VZONE_TO_PZONE: [[usize; 16]; 7] = [
|
||||||
|
[0, 1, 2, 9, 8, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10],
|
||||||
|
[0, 1, 2, 3, 10, 9, 8, 4, 5, 6, 7, 15, 14, 13, 12, 11],
|
||||||
|
[0, 1, 2, 3, 4, 11, 10, 9, 8, 5, 6, 7, 15, 14, 13, 12],
|
||||||
|
[0, 1, 2, 3, 4, 5, 12, 11, 10, 9, 8, 6, 7, 15, 14, 13],
|
||||||
|
[0, 1, 2, 3, 4, 5, 6, 13, 12, 11, 10, 9, 8, 7, 15, 14],
|
||||||
|
[0, 1, 2, 3, 4, 5, 6, 7, 14, 13, 12, 11, 10, 9, 8, 15],
|
||||||
|
[0, 1, 2, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10, 9, 8],
|
||||||
|
];
|
||||||
|
|
||||||
|
const ROM_ZONES: [usize; 7] = [5, 7, 9, 11, 13, 15, 16];
|
||||||
|
|
||||||
|
struct Mapping {
|
||||||
|
lba: usize,
|
||||||
|
offset: usize,
|
||||||
|
length: usize,
|
||||||
|
writable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Disk {
|
||||||
|
file: File,
|
||||||
|
format: Format,
|
||||||
|
mapping: HashMap<usize, Mapping>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Disk {
|
||||||
|
pub fn get_format(&self) -> &Format {
|
||||||
|
&self.format
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lba(&self, track: u32, head: u32, block: u32) -> Option<usize> {
|
||||||
|
if head == 0 && track < 12 {
|
||||||
|
return Some((track << 1 | block ^ (track % 2)) as usize);
|
||||||
|
}
|
||||||
|
let location = track << 2 | head << 1 | block;
|
||||||
|
self.mapping
|
||||||
|
.get(&(location as usize))
|
||||||
|
.map(|block| block.lba)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_block(
|
||||||
|
&mut self,
|
||||||
|
track: u32,
|
||||||
|
head: u32,
|
||||||
|
block: u32,
|
||||||
|
) -> Result<Option<Vec<u8>>, Error> {
|
||||||
|
let location = track << 2 | head << 1 | block;
|
||||||
|
if let Some(block) = self.mapping.get(&(location as usize)) {
|
||||||
|
let mut data = vec![0u8; block.length];
|
||||||
|
self.file.seek(SeekFrom::Start(block.offset as u64))?;
|
||||||
|
self.file.read_exact(&mut data)?;
|
||||||
|
return Ok(Some(data));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_block(
|
||||||
|
&mut self,
|
||||||
|
track: u32,
|
||||||
|
head: u32,
|
||||||
|
block: u32,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let location = track << 2 | head << 1 | block;
|
||||||
|
if let Some(block) = self.mapping.get(&(location as usize)) {
|
||||||
|
if block.length == data.len() && block.writable {
|
||||||
|
self.file.seek(SeekFrom::Start(block.offset as u64))?;
|
||||||
|
self.file.write_all(data)?;
|
||||||
|
return Ok(Some(()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(path: &str) -> Result<Disk, Error> {
|
||||||
|
let mut file = File::options().read(true).write(true).open(path)?;
|
||||||
|
let (format, mapping) = load_ndd(&mut file)?;
|
||||||
|
Ok(Disk {
|
||||||
|
file,
|
||||||
|
format,
|
||||||
|
mapping,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_multiple(paths: &[String]) -> Result<Vec<Disk>, Error> {
|
||||||
|
let mut disks: Vec<Disk> = Vec::new();
|
||||||
|
for path in paths {
|
||||||
|
let disk = open(path)?;
|
||||||
|
disks.push(disk);
|
||||||
|
}
|
||||||
|
if !disks.windows(2).all(|d| d[0].format == d[1].format) {
|
||||||
|
return Err(Error::new("Disk format mismatch"));
|
||||||
|
}
|
||||||
|
Ok(disks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_ndd(file: &mut File) -> Result<(Format, HashMap<usize, Mapping>), Error> {
|
||||||
|
let mut disk_format: Option<Format> = None;
|
||||||
|
let mut disk_type: usize = 0;
|
||||||
|
let mut sys_data = vec![0u8; SYSTEM_SECTOR_LENGTH];
|
||||||
|
let mut bad_lbas: Vec<usize> = Vec::new();
|
||||||
|
|
||||||
|
for info in SYSTEM_AREA {
|
||||||
|
bad_lbas.clear();
|
||||||
|
for &lba in info.sys_lba {
|
||||||
|
let data = load_sys_lba(file, lba)?;
|
||||||
|
if verify_sys_lba(&data, info.sector_length) {
|
||||||
|
if (data[4] != 0x10) || ((data[5] & 0xF0) != 0x10) {
|
||||||
|
bad_lbas.push(lba);
|
||||||
|
} else {
|
||||||
|
disk_format = Some(info.format);
|
||||||
|
disk_type = (data[5] & 0x0F) as usize;
|
||||||
|
sys_data = data[0..SYSTEM_SECTOR_LENGTH].to_vec();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bad_lbas.push(lba);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disk_format.is_some() {
|
||||||
|
bad_lbas.append(&mut info.bad_lba.to_vec());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disk_format.is_none() {
|
||||||
|
return Err(Error::new("Provided 64DD disk file is not valid"));
|
||||||
|
}
|
||||||
|
if disk_type >= VZONE_TO_PZONE.len() {
|
||||||
|
return Err(Error::new("Unknown disk type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut id_lba_valid = false;
|
||||||
|
for lba in ID_LBAS {
|
||||||
|
let data = load_sys_lba(file, lba)?;
|
||||||
|
let valid = verify_sys_lba(&data, SYSTEM_SECTOR_LENGTH);
|
||||||
|
if !valid {
|
||||||
|
bad_lbas.push(lba);
|
||||||
|
}
|
||||||
|
id_lba_valid |= valid;
|
||||||
|
}
|
||||||
|
if !id_lba_valid {
|
||||||
|
return Err(Error::new("No valid ID LBA found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut zone_bad_tracks: Vec<Vec<usize>> = Vec::new();
|
||||||
|
|
||||||
|
for (zone, info) in ZONE_MAPPING.iter().enumerate() {
|
||||||
|
let mut bad_tracks: Vec<usize> = Vec::new();
|
||||||
|
let start = if zone == 0 { 0 } else { sys_data[0x07 + zone] };
|
||||||
|
let stop = sys_data[0x07 + zone + 1];
|
||||||
|
for offset in start..stop {
|
||||||
|
bad_tracks.push(sys_data[0x20 + offset as usize] as usize);
|
||||||
|
}
|
||||||
|
for track in 0..(BAD_TRACKS_PER_ZONE - bad_tracks.len()) {
|
||||||
|
bad_tracks.push(info.tracks - track - 1);
|
||||||
|
}
|
||||||
|
zone_bad_tracks.push(bad_tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mapping = HashMap::new();
|
||||||
|
|
||||||
|
let mut lba: usize = 0;
|
||||||
|
let mut offset: usize = 0;
|
||||||
|
let mut starting_block: usize = 0;
|
||||||
|
|
||||||
|
for (vzone, &pzone) in VZONE_TO_PZONE[disk_type].iter().enumerate() {
|
||||||
|
let DiskZone {
|
||||||
|
head,
|
||||||
|
sector_length,
|
||||||
|
tracks,
|
||||||
|
track_offset,
|
||||||
|
} = ZONE_MAPPING[pzone];
|
||||||
|
|
||||||
|
let zone_tracks: Box<dyn Iterator<Item = usize>> = if head == 0 {
|
||||||
|
Box::new(0..tracks)
|
||||||
|
} else {
|
||||||
|
Box::new((0..tracks).rev())
|
||||||
|
};
|
||||||
|
|
||||||
|
for zone_track in zone_tracks {
|
||||||
|
if !zone_bad_tracks[pzone].contains(&zone_track) {
|
||||||
|
for block in 0..BLOCKS_PER_TRACK {
|
||||||
|
let track = track_offset + zone_track;
|
||||||
|
let location = (track << 2) | (head << 1) | (starting_block ^ block);
|
||||||
|
let length = sector_length * SECTORS_PER_BLOCK;
|
||||||
|
if !bad_lbas.contains(&lba) {
|
||||||
|
let writable = vzone >= ROM_ZONES[disk_type];
|
||||||
|
mapping.insert(
|
||||||
|
location,
|
||||||
|
Mapping {
|
||||||
|
lba,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
writable,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lba += 1;
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
starting_block ^= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((disk_format.unwrap(), mapping))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_sys_lba(file: &mut File, lba: usize) -> Result<Vec<u8>, Error> {
|
||||||
|
let length = SYSTEM_SECTOR_LENGTH * SECTORS_PER_BLOCK;
|
||||||
|
file.seek(SeekFrom::Start((lba * length) as u64))?;
|
||||||
|
let mut data = vec![0u8; length];
|
||||||
|
file.read_exact(&mut data)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_sys_lba(data: &[u8], sector_length: usize) -> bool {
|
||||||
|
let sys_data = &data[0..sector_length];
|
||||||
|
for sector in 1..SECTORS_PER_BLOCK {
|
||||||
|
let offset = sector * sector_length;
|
||||||
|
let verify_data = &data[offset..(offset + sector_length)];
|
||||||
|
if sys_data != verify_data {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
841
sw/deployer/src/main.rs
Normal file
841
sw/deployer/src/main.rs
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
mod debug;
|
||||||
|
mod disk;
|
||||||
|
mod n64;
|
||||||
|
mod sc64;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
|
use clap_num::maybe_hex_range;
|
||||||
|
use colored::Colorize;
|
||||||
|
use panic_message::panic_message;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{stdin, stdout, Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
{panic, process, thread},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
|
||||||
|
/// Connect to SC64 device on provided serial port
|
||||||
|
#[arg(short, long)]
|
||||||
|
port: Option<String>,
|
||||||
|
|
||||||
|
/// Connect to SC64 device on provided remote address
|
||||||
|
#[arg(short, long, conflicts_with = "port")]
|
||||||
|
remote: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// List connected SC64 devices
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// Upload ROM (and save) to the SC64
|
||||||
|
Upload(UploadArgs),
|
||||||
|
|
||||||
|
/// Download specific memory region and write it to file
|
||||||
|
Download {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: DownloadCommands,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Upload ROM (and save), 64DD IPL then run disk server
|
||||||
|
_64DD(_64DDArgs),
|
||||||
|
|
||||||
|
/// Enter debug mode
|
||||||
|
Debug(DebugArgs),
|
||||||
|
|
||||||
|
/// Dump data from arbitrary location in SC64 memory space
|
||||||
|
Dump(DumpArgs),
|
||||||
|
|
||||||
|
/// Print information about connected SC64 device
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Update persistent settings on SC64 device
|
||||||
|
Set {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: SetCommands,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Print firmware metadata / update or backup SC64 firmware
|
||||||
|
Firmware {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: FirmwareCommands,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Expose SC64 device over network
|
||||||
|
Server(ServerArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct UploadArgs {
|
||||||
|
/// Path to the ROM file
|
||||||
|
rom: PathBuf,
|
||||||
|
|
||||||
|
/// Path to the save file
|
||||||
|
#[arg(short, long)]
|
||||||
|
save: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Override autodetected save type
|
||||||
|
#[arg(short = 't', long)]
|
||||||
|
save_type: Option<SaveType>,
|
||||||
|
|
||||||
|
/// Use direct boot mode (skip bootloader)
|
||||||
|
#[arg(short, long)]
|
||||||
|
direct: bool,
|
||||||
|
|
||||||
|
/// Do not put last 128 kiB of ROM inside flash memory (can corrupt non EEPROM saves)
|
||||||
|
#[arg(short, long)]
|
||||||
|
no_shadow: bool,
|
||||||
|
|
||||||
|
/// Force TV type
|
||||||
|
#[arg(long, conflicts_with = "direct")]
|
||||||
|
tv: Option<TvType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum DownloadCommands {
|
||||||
|
/// Download save and write it to file
|
||||||
|
Save(DownloadArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct DownloadArgs {
|
||||||
|
/// Path to the file
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct _64DDArgs {
|
||||||
|
/// Path to the 64DD IPL file
|
||||||
|
ddipl: PathBuf,
|
||||||
|
|
||||||
|
/// Path to the 64DD disk file (.ndd format, can be specified multiple times)
|
||||||
|
#[arg(required = true)]
|
||||||
|
disk: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Path to the ROM file
|
||||||
|
#[arg(short, long)]
|
||||||
|
rom: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Path to the save file
|
||||||
|
#[arg(short, long, requires = "rom")]
|
||||||
|
save: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Override autodetected save type
|
||||||
|
#[arg(short = 't', long, requires = "rom")]
|
||||||
|
save_type: Option<SaveType>,
|
||||||
|
|
||||||
|
/// Use direct boot mode (skip bootloader)
|
||||||
|
#[arg(short, long)]
|
||||||
|
direct: bool,
|
||||||
|
|
||||||
|
/// Force TV type
|
||||||
|
#[arg(long, conflicts_with = "direct")]
|
||||||
|
tv: Option<TvType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct DebugArgs {
|
||||||
|
/// Enable IS-Viewer64 and set listening address at ROM offset (in most cases it's fixed at 0x03FF0000)
|
||||||
|
#[arg(long, value_name = "offset", value_parser = |s: &str| maybe_hex_range::<u32>(s, 0x00000004, 0x03FF0000))]
|
||||||
|
isv: Option<u32>,
|
||||||
|
|
||||||
|
/// Expose TCP socket port for GDB debugging
|
||||||
|
#[arg(long, value_name = "port", value_parser = clap::value_parser!(u16).range(1..))]
|
||||||
|
gdb: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct DumpArgs {
|
||||||
|
/// Starting memory address
|
||||||
|
#[arg(value_parser = |s: &str| maybe_hex_range::<u32>(s, 0, sc64::MEMORY_LENGTH as u32))]
|
||||||
|
address: u32,
|
||||||
|
|
||||||
|
/// Dump length
|
||||||
|
#[arg(value_parser = |s: &str| maybe_hex_range::<usize>(s, 1, sc64::MEMORY_LENGTH))]
|
||||||
|
length: usize,
|
||||||
|
|
||||||
|
/// Path to the dump file
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum SetCommands {
|
||||||
|
/// Synchronize real time clock (RTC) on the SC64 with local system time
|
||||||
|
Rtc,
|
||||||
|
|
||||||
|
/// Enable LED I/O activity blinking
|
||||||
|
BlinkOn,
|
||||||
|
|
||||||
|
/// Disable LED I/O activity blinking
|
||||||
|
BlinkOff,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum FirmwareCommands {
|
||||||
|
/// Print metadata included inside SC64 firmware file
|
||||||
|
Info(FirmwareArgs),
|
||||||
|
|
||||||
|
/// Download current SC64 firmware and save it to provided file
|
||||||
|
Backup(FirmwareArgs),
|
||||||
|
|
||||||
|
/// Update SC64 firmware from provided file
|
||||||
|
Update(FirmwareArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct FirmwareArgs {
|
||||||
|
/// Path to the firmware file
|
||||||
|
firmware: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct ServerArgs {
|
||||||
|
/// Listen on provided address:port
|
||||||
|
#[arg(default_value = "127.0.0.1:9064")]
|
||||||
|
address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, ValueEnum)]
|
||||||
|
enum SaveType {
|
||||||
|
None,
|
||||||
|
Eeprom4k,
|
||||||
|
Eeprom16k,
|
||||||
|
Sram,
|
||||||
|
SramBanked,
|
||||||
|
Flashram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<n64::SaveType> for SaveType {
|
||||||
|
fn from(value: n64::SaveType) -> Self {
|
||||||
|
match value {
|
||||||
|
n64::SaveType::None => Self::None,
|
||||||
|
n64::SaveType::Eeprom4k => Self::Eeprom4k,
|
||||||
|
n64::SaveType::Eeprom16k => Self::Eeprom16k,
|
||||||
|
n64::SaveType::Sram => Self::Sram,
|
||||||
|
n64::SaveType::SramBanked => Self::SramBanked,
|
||||||
|
n64::SaveType::Flashram => Self::Flashram,
|
||||||
|
n64::SaveType::Sram128kB => Self::Sram,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SaveType> for sc64::SaveType {
|
||||||
|
fn from(value: SaveType) -> Self {
|
||||||
|
match value {
|
||||||
|
SaveType::None => Self::None,
|
||||||
|
SaveType::Eeprom4k => Self::Eeprom4k,
|
||||||
|
SaveType::Eeprom16k => Self::Eeprom16k,
|
||||||
|
SaveType::Sram => Self::Sram,
|
||||||
|
SaveType::SramBanked => Self::SramBanked,
|
||||||
|
SaveType::Flashram => Self::Flashram,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, ValueEnum)]
|
||||||
|
enum TvType {
|
||||||
|
PAL,
|
||||||
|
NTSC,
|
||||||
|
MPAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TvType> for sc64::TvType {
|
||||||
|
fn from(value: TvType) -> Self {
|
||||||
|
match value {
|
||||||
|
TvType::PAL => Self::PAL,
|
||||||
|
TvType::NTSC => Self::NTSC,
|
||||||
|
TvType::MPAL => Self::MPAL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Connection {
|
||||||
|
Local(Option<String>),
|
||||||
|
Remote(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
panic::set_hook(Box::new(|_| {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
match panic::catch_unwind(|| handle_command(&cli.command, cli.port, cli.remote)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(payload) => {
|
||||||
|
eprintln!("{}", panic_message(&payload).red());
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_command(command: &Commands, port: Option<String>, remote: Option<String>) {
|
||||||
|
let connection = if let Some(remote) = remote {
|
||||||
|
Connection::Remote(remote)
|
||||||
|
} else {
|
||||||
|
Connection::Local(port)
|
||||||
|
};
|
||||||
|
let result = match command {
|
||||||
|
Commands::List => handle_list_command(),
|
||||||
|
Commands::Upload(args) => handle_upload_command(connection, args),
|
||||||
|
Commands::Download { command } => handle_download_command(connection, command),
|
||||||
|
Commands::_64DD(args) => handle_64dd_command(connection, args),
|
||||||
|
Commands::Debug(args) => handle_debug_command(connection, args),
|
||||||
|
Commands::Dump(args) => handle_dump_command(connection, args),
|
||||||
|
Commands::Info => handle_info_command(connection),
|
||||||
|
Commands::Set { command } => handle_set_command(connection, command),
|
||||||
|
Commands::Firmware { command } => handle_firmware_command(connection, command),
|
||||||
|
Commands::Server(args) => handle_server_command(connection, args),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => panic!("{error}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_list_command() -> Result<(), sc64::Error> {
|
||||||
|
let devices = sc64::list_local_devices()?;
|
||||||
|
|
||||||
|
println!("{}", "Found devices:".bold());
|
||||||
|
for (i, d) in devices.iter().enumerate() {
|
||||||
|
println!(" {i}: [{}] at port [{}]", d.serial_number, d.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_upload_command(connection: Connection, args: &UploadArgs) -> Result<(), sc64::Error> {
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
sc64.reset_state()?;
|
||||||
|
|
||||||
|
let (mut rom_file, rom_name, rom_length) = open_file(&args.rom)?;
|
||||||
|
|
||||||
|
log_wait(format!("Uploading ROM [{rom_name}]"), || {
|
||||||
|
sc64.upload_rom(&mut rom_file, rom_length, args.no_shadow)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let save: SaveType = if let Some(save_type) = args.save_type.clone() {
|
||||||
|
save_type
|
||||||
|
} else {
|
||||||
|
let (save_type, title) = n64::guess_save_type(&mut rom_file)?;
|
||||||
|
if let Some(title) = title {
|
||||||
|
println!("ROM title: {title}");
|
||||||
|
};
|
||||||
|
save_type.into()
|
||||||
|
};
|
||||||
|
let save_type: sc64::SaveType = save.into();
|
||||||
|
println!("Save type set to [{save_type}]");
|
||||||
|
sc64.set_save_type(save_type)?;
|
||||||
|
|
||||||
|
if args.save.is_some() {
|
||||||
|
let (mut save_file, save_name, save_length) = open_file(&args.save.as_ref().unwrap())?;
|
||||||
|
|
||||||
|
log_wait(format!("Uploading save [{save_name}]"), || {
|
||||||
|
sc64.upload_save(&mut save_file, save_length)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let boot_mode = if args.direct {
|
||||||
|
sc64::BootMode::DirectRom
|
||||||
|
} else {
|
||||||
|
sc64::BootMode::Rom
|
||||||
|
};
|
||||||
|
println!("Boot mode set to [{boot_mode}]");
|
||||||
|
sc64.set_boot_mode(boot_mode)?;
|
||||||
|
|
||||||
|
if let Some(tv) = args.tv.clone() {
|
||||||
|
let tv_type: sc64::TvType = tv.into();
|
||||||
|
println!("TV type set to [{tv_type}]");
|
||||||
|
sc64.set_tv_type(tv_type)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc64.calculate_cic_parameters()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_download_command(
|
||||||
|
connection: Connection,
|
||||||
|
command: &DownloadCommands,
|
||||||
|
) -> Result<(), sc64::Error> {
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
match command {
|
||||||
|
DownloadCommands::Save(args) => {
|
||||||
|
let (mut file, name) = create_file(&args.path)?;
|
||||||
|
|
||||||
|
log_wait(format!("Downloading save [{name}]"), || {
|
||||||
|
sc64.download_save(&mut file)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), sc64::Error> {
|
||||||
|
const MAX_ROM_LENGTH: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {} {}",
|
||||||
|
"[WARNING]:".bold().bright_yellow(),
|
||||||
|
"Do not use this mode when real 64DD accessory is connected to the N64.".bright_yellow(),
|
||||||
|
"This might permanently damage either 64DD or SC64.".bright_yellow()
|
||||||
|
);
|
||||||
|
|
||||||
|
sc64.reset_state()?;
|
||||||
|
|
||||||
|
if let Some(rom) = &args.rom {
|
||||||
|
let (mut rom_file, rom_name, rom_length) = open_file(rom)?;
|
||||||
|
if rom_length > MAX_ROM_LENGTH {
|
||||||
|
return Err(sc64::Error::new("ROM file size too big for 64DD mode"));
|
||||||
|
}
|
||||||
|
log_wait(format!("Uploading ROM [{rom_name}]"), || {
|
||||||
|
sc64.upload_rom(&mut rom_file, rom_length, false)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let save: SaveType = if let Some(save_type) = args.save_type.clone() {
|
||||||
|
save_type
|
||||||
|
} else {
|
||||||
|
let (save_type, title) = n64::guess_save_type(&mut rom_file)?;
|
||||||
|
if let Some(title) = title {
|
||||||
|
println!("ROM title: {title}");
|
||||||
|
};
|
||||||
|
save_type.into()
|
||||||
|
};
|
||||||
|
let save_type: sc64::SaveType = save.into();
|
||||||
|
println!("Save type set to [{save_type}]");
|
||||||
|
sc64.set_save_type(save_type)?;
|
||||||
|
|
||||||
|
if args.save.is_some() {
|
||||||
|
let (mut save_file, save_name, save_length) = open_file(&args.save.as_ref().unwrap())?;
|
||||||
|
|
||||||
|
log_wait(format!("Uploading save [{save_name}]"), || {
|
||||||
|
sc64.upload_save(&mut save_file, save_length)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut ddipl_file, ddipl_name, ddipl_length) = open_file(&args.ddipl)?;
|
||||||
|
|
||||||
|
log_wait(format!("Uploading DDIPL [{ddipl_name}]"), || {
|
||||||
|
sc64.upload_ddipl(&mut ddipl_file, ddipl_length)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let boot_mode = if args.rom.is_some() {
|
||||||
|
if args.direct {
|
||||||
|
sc64::BootMode::DirectRom
|
||||||
|
} else {
|
||||||
|
sc64::BootMode::Rom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if args.direct {
|
||||||
|
sc64::BootMode::DirectDdIpl
|
||||||
|
} else {
|
||||||
|
sc64::BootMode::DdIpl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("Boot mode set to [{boot_mode}]");
|
||||||
|
sc64.set_boot_mode(boot_mode)?;
|
||||||
|
|
||||||
|
if let Some(tv) = args.tv.clone() {
|
||||||
|
let tv_type: sc64::TvType = tv.into();
|
||||||
|
println!("TV type set to [{tv_type}]");
|
||||||
|
sc64.set_tv_type(tv_type)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc64.calculate_cic_parameters()?;
|
||||||
|
|
||||||
|
let disk_paths: Vec<String> = args
|
||||||
|
.disk
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
let disk_names: Vec<String> = args
|
||||||
|
.disk
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.file_name().unwrap().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut disks = disk::open_multiple(&disk_paths)?;
|
||||||
|
let mut selected_disk_index: usize = 0;
|
||||||
|
let mut selected_disk: Option<&mut disk::Disk> = None;
|
||||||
|
|
||||||
|
let drive_type = match disks[0].get_format() {
|
||||||
|
disk::Format::Retail => sc64::DdDriveType::Retail,
|
||||||
|
disk::Format::Development => sc64::DdDriveType::Development,
|
||||||
|
};
|
||||||
|
|
||||||
|
sc64.configure_64dd(sc64::DdMode::Full, drive_type)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Press button on the SC64 device to cycle through provided disks".bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
let exit = setup_exit_flag();
|
||||||
|
while !exit.load(Ordering::Relaxed) {
|
||||||
|
if let Some(data_packet) = sc64.receive_data_packet()? {
|
||||||
|
match data_packet {
|
||||||
|
sc64::DataPacket::Disk(mut packet) => {
|
||||||
|
let track = packet.info.track;
|
||||||
|
let head = packet.info.head;
|
||||||
|
let block = packet.info.block;
|
||||||
|
if let Some(ref mut disk) = selected_disk {
|
||||||
|
let reply_packet = match packet.kind {
|
||||||
|
sc64::DiskPacketKind::Read => {
|
||||||
|
print!("{}", "[R]".cyan());
|
||||||
|
disk.read_block(track, head, block)?.map(|data| {
|
||||||
|
packet.info.set_data(&data);
|
||||||
|
packet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sc64::DiskPacketKind::Write => {
|
||||||
|
print!("{}", "[W]".yellow());
|
||||||
|
let data = &packet.info.data;
|
||||||
|
disk.write_block(track, head, block, data)?.map(|_| packet)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let lba = if let Some(lba) = disk.get_lba(track, head, block) {
|
||||||
|
format!("{lba}")
|
||||||
|
} else {
|
||||||
|
"Invalid".to_string()
|
||||||
|
};
|
||||||
|
let message = format!(" {track:4}:{head}:{block} / LBA: {lba}");
|
||||||
|
if reply_packet.is_some() {
|
||||||
|
println!("{}", message.green());
|
||||||
|
} else {
|
||||||
|
println!("{}", message.red());
|
||||||
|
}
|
||||||
|
sc64.reply_disk_packet(reply_packet)?;
|
||||||
|
} else {
|
||||||
|
sc64.reply_disk_packet(None)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sc64::DataPacket::Button => {
|
||||||
|
if selected_disk.is_some() {
|
||||||
|
sc64.set_64dd_disk_state(sc64::DdDiskState::Ejected)?;
|
||||||
|
selected_disk = None;
|
||||||
|
println!("64DD disk ejected [{}]", disk_names[selected_disk_index]);
|
||||||
|
} else {
|
||||||
|
selected_disk_index += 1;
|
||||||
|
if selected_disk_index >= disks.len() {
|
||||||
|
selected_disk_index = 0;
|
||||||
|
}
|
||||||
|
selected_disk = Some(&mut disks[selected_disk_index]);
|
||||||
|
sc64.set_64dd_disk_state(sc64::DdDiskState::Inserted)?;
|
||||||
|
println!("64DD disk inserted [{}]", disk_names[selected_disk_index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_debug_command(connection: Connection, args: &DebugArgs) -> Result<(), sc64::Error> {
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
let mut debug_handler = debug::new(args.gdb)?;
|
||||||
|
if let Some(port) = args.gdb {
|
||||||
|
println!("GDB TCP socket listening at [0.0.0.0:{port}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.isv.is_some() {
|
||||||
|
sc64.configure_is_viewer_64(args.isv)?;
|
||||||
|
println!(
|
||||||
|
"IS-Viewer 64 configured and listening at ROM offset [0x{:08X}]",
|
||||||
|
args.isv.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "Debug mode started".bold());
|
||||||
|
|
||||||
|
let exit = setup_exit_flag();
|
||||||
|
while !exit.load(Ordering::Relaxed) {
|
||||||
|
if let Some(data_packet) = sc64.receive_data_packet()? {
|
||||||
|
match data_packet {
|
||||||
|
sc64::DataPacket::IsViewer(message) => {
|
||||||
|
print!("{message}")
|
||||||
|
}
|
||||||
|
sc64::DataPacket::Debug(debug_packet) => {
|
||||||
|
debug_handler.handle_debug_packet(debug_packet)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else if let Some(gdb_packet) = debug_handler.receive_gdb_packet() {
|
||||||
|
sc64.send_debug_packet(gdb_packet)?;
|
||||||
|
} else if let Some(debug_packet) = debug_handler.process_user_input() {
|
||||||
|
sc64.send_debug_packet(debug_packet)?;
|
||||||
|
} else {
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "Debug mode ended".bold());
|
||||||
|
|
||||||
|
if args.isv.is_some() {
|
||||||
|
sc64.configure_is_viewer_64(None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_dump_command(connection: Connection, args: &DumpArgs) -> Result<(), sc64::Error> {
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
let (mut dump_file, dump_name) = create_file(&args.path)?;
|
||||||
|
|
||||||
|
log_wait(
|
||||||
|
format!(
|
||||||
|
"Dumping from [0x{:08X}] length [0x{:X}] to [{dump_name}]",
|
||||||
|
args.address, args.length
|
||||||
|
),
|
||||||
|
|| sc64.dump_memory(&mut dump_file, args.address, args.length),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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 %Z");
|
||||||
|
|
||||||
|
println!("{}", "SC64 information and current state:".bold());
|
||||||
|
println!(" Firmware version: v{}.{}.{}", major, minor, revision);
|
||||||
|
println!(" RTC datetime: {}", datetime);
|
||||||
|
println!(" Boot mode: {}", state.boot_mode);
|
||||||
|
println!(" Save type: {}", state.save_type);
|
||||||
|
println!(" CIC seed: {}", state.cic_seed);
|
||||||
|
println!(" TV type: {}", state.tv_type);
|
||||||
|
println!(" Bootloader switch: {}", state.bootloader_switch);
|
||||||
|
println!(" ROM write: {}", state.rom_write_enable);
|
||||||
|
println!(" ROM shadow: {}", state.rom_shadow_enable);
|
||||||
|
println!(" ROM extended: {}", state.rom_extended_enable);
|
||||||
|
println!(" IS-Viewer 64 offset: 0x{:08X}", state.isv_address);
|
||||||
|
println!(" 64DD mode: {}", state.dd_mode);
|
||||||
|
println!(" 64DD SD card mode: {}", state.dd_sd_enable);
|
||||||
|
println!(" 64DD drive type: {}", state.dd_drive_type);
|
||||||
|
println!(" 64DD disk state: {}", state.dd_disk_state);
|
||||||
|
println!(" Button mode: {}", state.button_mode);
|
||||||
|
println!(" Button state: {}", state.button_state);
|
||||||
|
println!(" LED blink: {}", state.led_enable);
|
||||||
|
println!(" FPGA debug data: {}", state.fpga_debug_data);
|
||||||
|
println!(" MCU stack usage: {}", state.mcu_stack_usage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_set_command(connection: Connection, command: &SetCommands) -> Result<(), sc64::Error> {
|
||||||
|
let mut sc64 = init_sc64(connection, true)?;
|
||||||
|
|
||||||
|
match command {
|
||||||
|
SetCommands::Rtc => {
|
||||||
|
let datetime = Local::now();
|
||||||
|
sc64.set_datetime(datetime)?;
|
||||||
|
println!(
|
||||||
|
"SC64 RTC datetime synchronized to: {}",
|
||||||
|
datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string().green()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCommands::BlinkOn => {
|
||||||
|
sc64.set_led_blink(true)?;
|
||||||
|
println!(
|
||||||
|
"SC64 LED I/O activity blinking set to {}",
|
||||||
|
"enabled".green()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCommands::BlinkOff => {
|
||||||
|
sc64.set_led_blink(false)?;
|
||||||
|
println!("SC64 LED I/O activity blinking set to {}", "disabled".red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_firmware_command(
|
||||||
|
connection: Connection,
|
||||||
|
command: &FirmwareCommands,
|
||||||
|
) -> Result<(), sc64::Error> {
|
||||||
|
match command {
|
||||||
|
FirmwareCommands::Info(args) => {
|
||||||
|
let (mut firmware_file, _, firmware_length) = open_file(&args.firmware)?;
|
||||||
|
|
||||||
|
let mut firmware = vec![0u8; firmware_length as usize];
|
||||||
|
firmware_file.read_exact(&mut firmware)?;
|
||||||
|
|
||||||
|
let metadata = sc64::firmware::verify(&firmware)?;
|
||||||
|
println!("{}", "Firmware metadata:".bold());
|
||||||
|
println!("{}", format!("{}", metadata).cyan().to_string());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
FirmwareCommands::Backup(args) => {
|
||||||
|
let mut sc64 = init_sc64(connection, false)?;
|
||||||
|
|
||||||
|
let (mut backup_file, backup_name) = create_file(&args.firmware)?;
|
||||||
|
|
||||||
|
sc64.reset_state()?;
|
||||||
|
|
||||||
|
let firmware = log_wait(
|
||||||
|
format!("Generating firmware backup, this might take a while [{backup_name}]"),
|
||||||
|
|| sc64.backup_firmware(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let metadata = sc64::firmware::verify(&firmware)?;
|
||||||
|
println!("{}", "Firmware metadata:".bold());
|
||||||
|
println!("{}", format!("{}", metadata).cyan().to_string());
|
||||||
|
|
||||||
|
backup_file.write_all(&firmware)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
FirmwareCommands::Update(args) => {
|
||||||
|
let mut sc64 = init_sc64(connection, false)?;
|
||||||
|
|
||||||
|
let (mut update_file, update_name, update_length) = open_file(&args.firmware)?;
|
||||||
|
|
||||||
|
let mut firmware = vec![0u8; update_length as usize];
|
||||||
|
update_file.read_exact(&mut firmware)?;
|
||||||
|
|
||||||
|
let metadata = sc64::firmware::verify(&firmware)?;
|
||||||
|
println!("{}", "Firmware metadata:".bold());
|
||||||
|
println!("{}", format!("{}", metadata).cyan().to_string());
|
||||||
|
println!("{}", "Firmware file verification was successful".green());
|
||||||
|
let answer = prompt(format!("{}", "Continue with update process? [y/N] ".bold()));
|
||||||
|
if answer.to_ascii_lowercase() != "y" {
|
||||||
|
println!("{}", "Firmware update process aborted".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Do not unplug SC64 from the computer, doing so might brick your device".yellow()
|
||||||
|
);
|
||||||
|
|
||||||
|
sc64.reset_state()?;
|
||||||
|
|
||||||
|
log_wait(
|
||||||
|
format!("Updating firmware, this might take a while [{update_name}]"),
|
||||||
|
|| sc64.update_firmware(&firmware),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_server_command(connection: Connection, args: &ServerArgs) -> Result<(), sc64::Error> {
|
||||||
|
let port = if let Connection::Local(port) = connection {
|
||||||
|
port
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
sc64::run_server(port, args.address.clone(), |event| match event {
|
||||||
|
sc64::ServerEvent::Listening(address) => {
|
||||||
|
println!("{}: Listening on address [{}]", "[Server]".bold(), address)
|
||||||
|
}
|
||||||
|
sc64::ServerEvent::Connection(peer) => {
|
||||||
|
println!("{}: New connection from [{}]", "[Server]".bold(), peer);
|
||||||
|
}
|
||||||
|
sc64::ServerEvent::Disconnected(peer) => {
|
||||||
|
println!("{}: Client disconnected [{}]", "[Server]".bold(), peer);
|
||||||
|
}
|
||||||
|
sc64::ServerEvent::Err(error) => {
|
||||||
|
println!(
|
||||||
|
"{}: Client disconnected with error: {}",
|
||||||
|
"[Server]".bold(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_sc64(connection: Connection, check_firmware: bool) -> Result<sc64::SC64, sc64::Error> {
|
||||||
|
let mut sc64 = match connection {
|
||||||
|
Connection::Local(port) => sc64::new_local(port),
|
||||||
|
Connection::Remote(remote) => sc64::new_remote(remote),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if check_firmware {
|
||||||
|
sc64.check_firmware_version()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sc64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_wait<F: FnOnce() -> Result<T, E>, T, E>(message: String, operation: F) -> Result<T, E> {
|
||||||
|
print!("{}... ", message);
|
||||||
|
stdout().flush().unwrap();
|
||||||
|
let result = operation();
|
||||||
|
if result.is_ok() {
|
||||||
|
println!("done");
|
||||||
|
} else {
|
||||||
|
println!("error!");
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(message: String) -> String {
|
||||||
|
print!("{message}");
|
||||||
|
stdout().flush().unwrap();
|
||||||
|
let mut answer = String::new();
|
||||||
|
stdin().read_line(&mut answer).unwrap();
|
||||||
|
answer.trim_end().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file(path: &PathBuf) -> Result<(File, String, usize), sc64::Error> {
|
||||||
|
let name: String = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let length = file.metadata()?.len() as usize;
|
||||||
|
Ok((file, name, length))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_file(path: &PathBuf) -> Result<(File, String), sc64::Error> {
|
||||||
|
let name: String = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let file = File::create(path)?;
|
||||||
|
Ok((file, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_exit_flag() -> Arc<AtomicBool> {
|
||||||
|
let exit_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
let handler_exit_flag = exit_flag.clone();
|
||||||
|
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
handler_exit_flag.store(true, Ordering::Relaxed);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
exit_flag
|
||||||
|
}
|
88
sw/deployer/src/n64.rs
Normal file
88
sw/deployer/src/n64.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use include_flate::flate;
|
||||||
|
use std::io::{Error, Read, Seek, SeekFrom};
|
||||||
|
|
||||||
|
flate!(static MUPEN64PLUS_INI: str from "data/mupen64plus.ini");
|
||||||
|
|
||||||
|
pub enum SaveType {
|
||||||
|
None,
|
||||||
|
Eeprom4k,
|
||||||
|
Eeprom16k,
|
||||||
|
Sram,
|
||||||
|
SramBanked,
|
||||||
|
Flashram,
|
||||||
|
Sram128kB,
|
||||||
|
}
|
||||||
|
|
||||||
|
const HASH_CHUNK_LENGTH: usize = 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
pub fn guess_save_type<T: Read + Seek>(
|
||||||
|
reader: &mut T,
|
||||||
|
) -> Result<(SaveType, Option<String>), Error> {
|
||||||
|
let mut ed64_header = vec![0u8; 4];
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::Start(0x3C))?;
|
||||||
|
reader.read_exact(&mut ed64_header)?;
|
||||||
|
|
||||||
|
if &ed64_header[0..2] == b"ED" {
|
||||||
|
return Ok((
|
||||||
|
match ed64_header[3] >> 4 {
|
||||||
|
1 => SaveType::Eeprom4k,
|
||||||
|
2 => SaveType::Eeprom16k,
|
||||||
|
3 => SaveType::Sram,
|
||||||
|
4 => SaveType::SramBanked,
|
||||||
|
5 => SaveType::Flashram,
|
||||||
|
6 => SaveType::Sram128kB,
|
||||||
|
_ => SaveType::None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pi_config = vec![0u8; 4];
|
||||||
|
|
||||||
|
reader.rewind()?;
|
||||||
|
reader.read_exact(&mut pi_config)?;
|
||||||
|
|
||||||
|
let endian_swapper = match &pi_config[0..4] {
|
||||||
|
[0x37, 0x80, 0x40, 0x12] => |b: &mut [u8]| b.chunks_exact_mut(2).for_each(|c| c.swap(0, 1)),
|
||||||
|
[0x40, 0x12, 0x37, 0x80] => |b: &mut [u8]| {
|
||||||
|
b.chunks_exact_mut(4).for_each(|c| {
|
||||||
|
c.swap(0, 3);
|
||||||
|
c.swap(1, 2)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_ => |_: &mut [u8]| {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hasher = md5::Context::new();
|
||||||
|
let mut buffer = vec![0u8; HASH_CHUNK_LENGTH];
|
||||||
|
|
||||||
|
reader.rewind()?;
|
||||||
|
loop {
|
||||||
|
let chunk = reader.read(&mut buffer)?;
|
||||||
|
if chunk > 0 {
|
||||||
|
endian_swapper(&mut buffer[0..chunk]);
|
||||||
|
hasher.consume(&buffer[0..chunk]);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = hex::encode_upper(hasher.compute().0);
|
||||||
|
|
||||||
|
let database = ini::Ini::load_from_str(MUPEN64PLUS_INI.as_str())
|
||||||
|
.expect("Error during mupen64plus.ini parse operation");
|
||||||
|
if let Some(section) = database.section(Some(hash)) {
|
||||||
|
let save_type = section.get("SaveType").map_or(SaveType::None, |t| match t {
|
||||||
|
"Eeprom 4KB" => SaveType::Eeprom4k,
|
||||||
|
"Eeprom 16KB" => SaveType::Eeprom16k,
|
||||||
|
"SRAM" => SaveType::Sram,
|
||||||
|
"Flash RAM" => SaveType::Flashram,
|
||||||
|
_ => SaveType::None,
|
||||||
|
});
|
||||||
|
let title = section.get("GoodName").map(|s| s.to_string());
|
||||||
|
return Ok((save_type, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((SaveType::None, None))
|
||||||
|
}
|
220
sw/deployer/src/sc64/cic.rs
Normal file
220
sw/deployer/src/sc64/cic.rs
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
use super::Error;
|
||||||
|
|
||||||
|
pub const IPL3_OFFSET: u32 = 0x40;
|
||||||
|
pub const IPL3_LENGTH: usize = 0xFC0;
|
||||||
|
|
||||||
|
enum CicType {
|
||||||
|
_5101,
|
||||||
|
_6101,
|
||||||
|
_7102,
|
||||||
|
X102,
|
||||||
|
X103,
|
||||||
|
X105,
|
||||||
|
X106,
|
||||||
|
_5167,
|
||||||
|
NDXJ0,
|
||||||
|
NDDJ0,
|
||||||
|
NDDJ1,
|
||||||
|
NDDJ2,
|
||||||
|
NDDE0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for CicType {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
match value {
|
||||||
|
0x587BD543 => CicType::_5101,
|
||||||
|
0x6170A4A1 => CicType::_6101,
|
||||||
|
0x009E9EA3 => CicType::_7102,
|
||||||
|
0x90BB6CB5 => CicType::X102,
|
||||||
|
0x0B050EE0 => CicType::X103,
|
||||||
|
0x98BC2C86 => CicType::X105,
|
||||||
|
0xACC8580A => CicType::X106,
|
||||||
|
0x0E018159 => CicType::_5167,
|
||||||
|
0x10C68B18 => CicType::NDXJ0,
|
||||||
|
0xBC605D0A => CicType::NDDJ0,
|
||||||
|
0x502C4466 => CicType::NDDJ1,
|
||||||
|
0x0C965795 => CicType::NDDJ2,
|
||||||
|
0x8FEBA21E => CicType::NDDE0,
|
||||||
|
_ => CicType::X102,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CicType> for u8 {
|
||||||
|
fn from(value: CicType) -> Self {
|
||||||
|
match value {
|
||||||
|
CicType::_5101 => 0xAC,
|
||||||
|
CicType::_6101 => 0x3F,
|
||||||
|
CicType::_7102 => 0x3F,
|
||||||
|
CicType::X102 => 0x3F,
|
||||||
|
CicType::X103 => 0x78,
|
||||||
|
CicType::X105 => 0x91,
|
||||||
|
CicType::X106 => 0x85,
|
||||||
|
CicType::_5167 => 0xDD,
|
||||||
|
CicType::NDXJ0 => 0xDD,
|
||||||
|
CicType::NDDJ0 => 0xDD,
|
||||||
|
CicType::NDDJ1 => 0xDD,
|
||||||
|
CicType::NDDJ2 => 0xDD,
|
||||||
|
CicType::NDDE0 => 0xDE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guess_ipl3_seed(ipl3: &[u8]) -> Result<u8, Error> {
|
||||||
|
if ipl3.len() < IPL3_LENGTH {
|
||||||
|
return Err(Error::new("Invalid IPL3 length provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cic_type: CicType = crc32fast::hash(ipl3).into();
|
||||||
|
|
||||||
|
Ok(cic_type.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_ipl3_checksum(ipl3: &[u8], seed: u8) -> Result<[u8; 6], Error> {
|
||||||
|
if ipl3.len() < IPL3_LENGTH {
|
||||||
|
return Err(Error::new("Invalid IPL3 length provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAGIC: u32 = 0x6C078965;
|
||||||
|
|
||||||
|
let get = |offset: u32| {
|
||||||
|
let o: usize = offset as usize * 4;
|
||||||
|
return ((ipl3[o] as u32) << 24)
|
||||||
|
| ((ipl3[o + 1] as u32) << 16)
|
||||||
|
| ((ipl3[o + 2] as u32) << 8)
|
||||||
|
| (ipl3[o + 3] as u32);
|
||||||
|
};
|
||||||
|
let add = |a1: u32, a2: u32| u32::wrapping_add(a1, a2);
|
||||||
|
let sub = |a1: u32, a2: u32| u32::wrapping_sub(a1, a2);
|
||||||
|
let mul = |a1: u32, a2: u32| u32::wrapping_mul(a1, a2);
|
||||||
|
let lsh = |a: u32, s: u32| if s >= 32 { 0 } else { u32::wrapping_shl(a, s) };
|
||||||
|
let rsh = |a: u32, s: u32| if s >= 32 { 0 } else { u32::wrapping_shr(a, s) };
|
||||||
|
let checksum = |a0: u32, a1: u32, a2: u32| {
|
||||||
|
let prod = (a0 as u64).wrapping_mul(if a1 == 0 { a2 as u64 } else { a1 as u64 });
|
||||||
|
let hi = ((prod >> 32) & 0xFFFFFFFF) as u32;
|
||||||
|
let lo = (prod & 0xFFFFFFFF) as u32;
|
||||||
|
let diff = hi.wrapping_sub(lo);
|
||||||
|
return if diff == 0 { a0 } else { diff };
|
||||||
|
};
|
||||||
|
|
||||||
|
let init = add(mul(MAGIC, seed as u32), 1) ^ get(0);
|
||||||
|
|
||||||
|
let mut buffer = vec![init; 16];
|
||||||
|
|
||||||
|
for i in 1..=1008 as u32 {
|
||||||
|
let data_prev = get(i.saturating_sub(2));
|
||||||
|
let data_curr = get(i - 1);
|
||||||
|
|
||||||
|
buffer[0] = add(buffer[0], checksum(sub(1007, i), data_curr, i));
|
||||||
|
buffer[1] = checksum(buffer[1], data_curr, i);
|
||||||
|
buffer[2] = buffer[2] ^ data_curr;
|
||||||
|
buffer[3] = add(buffer[3], checksum(add(data_curr, 5), MAGIC, i));
|
||||||
|
|
||||||
|
let shift = data_prev & 0x1F;
|
||||||
|
let data_left = lsh(data_curr, 32 - shift);
|
||||||
|
let data_right = rsh(data_curr, shift);
|
||||||
|
let b4_shifted = data_left | data_right;
|
||||||
|
buffer[4] = add(buffer[4], b4_shifted);
|
||||||
|
|
||||||
|
let shift = rsh(data_prev, 27);
|
||||||
|
let data_left = lsh(data_curr, shift);
|
||||||
|
let data_right = rsh(data_curr, 32 - shift);
|
||||||
|
let b5_shifted = data_left | data_right;
|
||||||
|
buffer[5] = add(buffer[5], b5_shifted);
|
||||||
|
|
||||||
|
if data_curr < buffer[6] {
|
||||||
|
buffer[6] = add(buffer[3], buffer[6]) ^ add(data_curr, i);
|
||||||
|
} else {
|
||||||
|
buffer[6] = add(buffer[4], data_curr) ^ buffer[6];
|
||||||
|
}
|
||||||
|
|
||||||
|
let shift = data_prev & 0x1F;
|
||||||
|
let data_left = lsh(data_curr, shift);
|
||||||
|
let data_right = rsh(data_curr, 32 - shift);
|
||||||
|
buffer[7] = checksum(buffer[7], data_left | data_right, i);
|
||||||
|
|
||||||
|
let shift = rsh(data_prev, 27);
|
||||||
|
let data_left = lsh(data_curr, 32 - shift);
|
||||||
|
let data_right = rsh(data_curr, shift);
|
||||||
|
buffer[8] = checksum(buffer[8], data_left | data_right, i);
|
||||||
|
|
||||||
|
if data_prev < data_curr {
|
||||||
|
buffer[9] = checksum(buffer[9], data_curr, i)
|
||||||
|
} else {
|
||||||
|
buffer[9] = add(buffer[9], data_curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 1008 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_next = get(i);
|
||||||
|
|
||||||
|
buffer[10] = checksum(add(buffer[10], data_curr), data_next, i);
|
||||||
|
buffer[11] = checksum(buffer[11] ^ data_curr, data_next, i);
|
||||||
|
buffer[12] = add(buffer[12], buffer[8] ^ data_curr);
|
||||||
|
|
||||||
|
let shift = data_curr & 0x1F;
|
||||||
|
let data_left = lsh(data_curr, 32 - shift);
|
||||||
|
let data_right = rsh(data_curr, shift);
|
||||||
|
let tmp = data_left | data_right;
|
||||||
|
let shift = data_next & 0x1F;
|
||||||
|
let data_left = lsh(data_next, 32 - shift);
|
||||||
|
let data_right = rsh(data_next, shift);
|
||||||
|
buffer[13] = add(buffer[13], add(tmp, data_left | data_right));
|
||||||
|
|
||||||
|
let shift = data_curr & 0x1F;
|
||||||
|
let data_left = lsh(data_next, 32 - shift);
|
||||||
|
let data_right = rsh(data_next, shift);
|
||||||
|
let sum = checksum(buffer[14], b4_shifted, i);
|
||||||
|
buffer[14] = checksum(sum, data_left | data_right, i);
|
||||||
|
|
||||||
|
let shift = rsh(data_curr, 27);
|
||||||
|
let data_left = lsh(data_next, shift);
|
||||||
|
let data_right = rsh(data_next, 32 - shift);
|
||||||
|
let sum = checksum(buffer[15], b5_shifted, i);
|
||||||
|
buffer[15] = checksum(sum, data_left | data_right, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut final_buffer = vec![buffer[0]; 4];
|
||||||
|
|
||||||
|
for i in 0..16 as u32 {
|
||||||
|
let data = buffer[i as usize];
|
||||||
|
|
||||||
|
let shift = data & 0x1F;
|
||||||
|
let data_left = lsh(data, 32 - shift);
|
||||||
|
let data_right = rsh(data, shift);
|
||||||
|
let b0_shifted = add(final_buffer[0], data_left | data_right);
|
||||||
|
final_buffer[0] = b0_shifted;
|
||||||
|
|
||||||
|
if data < b0_shifted {
|
||||||
|
final_buffer[1] = add(final_buffer[1], data);
|
||||||
|
} else {
|
||||||
|
final_buffer[1] = checksum(final_buffer[1], data, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsh(data & 0x02, 1) == (data & 0x01) {
|
||||||
|
final_buffer[2] = add(final_buffer[2], data);
|
||||||
|
} else {
|
||||||
|
final_buffer[2] = checksum(final_buffer[2], data, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data & 0x01) == 0x01 {
|
||||||
|
final_buffer[3] = final_buffer[3] ^ data;
|
||||||
|
} else {
|
||||||
|
final_buffer[3] = checksum(final_buffer[3], data, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = checksum(final_buffer[0], final_buffer[1], 16);
|
||||||
|
let xor = final_buffer[3] ^ final_buffer[2];
|
||||||
|
|
||||||
|
Ok([
|
||||||
|
(sum >> 8) as u8,
|
||||||
|
(sum >> 0) as u8,
|
||||||
|
(xor >> 24) as u8,
|
||||||
|
(xor >> 16) as u8,
|
||||||
|
(xor >> 8) as u8,
|
||||||
|
(xor >> 0) as u8,
|
||||||
|
])
|
||||||
|
}
|
34
sw/deployer/src/sc64/error.rs
Normal file
34
sw/deployer/src/sc64/error.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Error {
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn new(description: &str) -> Self {
|
||||||
|
Error {
|
||||||
|
description: format!("{}", description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "SC64 error: {}", self.description.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Error::new(format!("IO error: {}", value).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serialport::Error> for Error {
|
||||||
|
fn from(value: serialport::Error) -> Self {
|
||||||
|
Error::new(format!("SerialPort error: {}", value.description).as_str())
|
||||||
|
}
|
||||||
|
}
|
149
sw/deployer/src/sc64/firmware.rs
Normal file
149
sw/deployer/src/sc64/firmware.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use super::Error;
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
io::{Cursor, Read, Seek, SeekFrom},
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ChunkId {
|
||||||
|
UpdateInfo,
|
||||||
|
McuData,
|
||||||
|
FpgaData,
|
||||||
|
BootloaderData,
|
||||||
|
PrimerData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for ChunkId {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
1 => Self::UpdateInfo,
|
||||||
|
2 => Self::McuData,
|
||||||
|
3 => Self::FpgaData,
|
||||||
|
4 => Self::BootloaderData,
|
||||||
|
5 => Self::PrimerData,
|
||||||
|
_ => return Err(Error::new("Unknown chunk id inside firmware update file")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ChunkId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
ChunkId::UpdateInfo => "Update info",
|
||||||
|
ChunkId::McuData => "MCU data",
|
||||||
|
ChunkId::FpgaData => "FPGA data",
|
||||||
|
ChunkId::BootloaderData => "Bootloader data",
|
||||||
|
ChunkId::PrimerData => "Primer data",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Firmware {
|
||||||
|
update_info: Option<String>,
|
||||||
|
mcu_data: Option<Vec<u8>>,
|
||||||
|
fpga_data: Option<Vec<u8>>,
|
||||||
|
bootloader_data: Option<Vec<u8>>,
|
||||||
|
primer_data: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Firmware {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(update_info) = &self.update_info {
|
||||||
|
f.write_fmt(format_args!("{}", &update_info))?;
|
||||||
|
} else {
|
||||||
|
f.write_str("No update info data included")?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.mcu_data {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"\nMCU data present, length: 0x{:X}",
|
||||||
|
data.len()
|
||||||
|
))?;
|
||||||
|
} else {
|
||||||
|
f.write_str("\nNo MCU data included")?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.fpga_data {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"\nFPGA data present, length: 0x{:X}",
|
||||||
|
data.len()
|
||||||
|
))?;
|
||||||
|
} else {
|
||||||
|
f.write_str("\nNo FPGA data included")?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.bootloader_data {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"\nBootloader data present, length: 0x{:X}",
|
||||||
|
data.len()
|
||||||
|
))?;
|
||||||
|
} else {
|
||||||
|
f.write_str("\nNo bootloader data included")?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.primer_data {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"\nPrimer data present, length: 0x{:X}",
|
||||||
|
data.len()
|
||||||
|
))?;
|
||||||
|
} else {
|
||||||
|
f.write_str("\nNo primer data included")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SC64_FIRMWARE_UPDATE_TOKEN: &[u8; 16] = b"SC64 Update v2.0";
|
||||||
|
|
||||||
|
pub fn verify(data: &[u8]) -> Result<Firmware, Error> {
|
||||||
|
let mut buffer = vec![0u8; 16];
|
||||||
|
|
||||||
|
let mut reader = Cursor::new(data);
|
||||||
|
|
||||||
|
reader.read(&mut buffer)?;
|
||||||
|
if buffer != SC64_FIRMWARE_UPDATE_TOKEN {
|
||||||
|
return Err(Error::new("Invalid firmware update header"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut firmware = Firmware {
|
||||||
|
update_info: None,
|
||||||
|
mcu_data: None,
|
||||||
|
fpga_data: None,
|
||||||
|
bootloader_data: None,
|
||||||
|
primer_data: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes = reader.read(&mut buffer)?;
|
||||||
|
if bytes == 16 {
|
||||||
|
let id: ChunkId = u32::from_le_bytes(buffer[0..4].try_into().unwrap()).try_into()?;
|
||||||
|
let aligned_length = u32::from_le_bytes(buffer[4..8].try_into().unwrap());
|
||||||
|
let checksum = u32::from_le_bytes(buffer[8..12].try_into().unwrap());
|
||||||
|
let data_length = u32::from_le_bytes(buffer[12..16].try_into().unwrap());
|
||||||
|
|
||||||
|
let mut data = vec![0u8; data_length as usize];
|
||||||
|
reader.read(&mut data)?;
|
||||||
|
|
||||||
|
let align = aligned_length - 4 - 4 - data_length;
|
||||||
|
reader.seek(SeekFrom::Current(align as i64))?;
|
||||||
|
|
||||||
|
if crc32fast::hash(&data) != checksum {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!("Invalid checksum for chunk [{id}]").as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match id {
|
||||||
|
ChunkId::UpdateInfo => {
|
||||||
|
firmware.update_info = Some(String::from_utf8_lossy(&data).to_string())
|
||||||
|
}
|
||||||
|
ChunkId::McuData => firmware.mcu_data = Some(data),
|
||||||
|
ChunkId::FpgaData => firmware.fpga_data = Some(data),
|
||||||
|
ChunkId::BootloaderData => firmware.bootloader_data = Some(data),
|
||||||
|
ChunkId::PrimerData => firmware.primer_data = Some(data),
|
||||||
|
}
|
||||||
|
} else if bytes == 0 {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return Err(Error::new("Unexpected end of data in firmware update"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(firmware)
|
||||||
|
}
|
505
sw/deployer/src/sc64/link.rs
Normal file
505
sw/deployer/src/sc64/link.rs
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
use super::error::Error;
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Write},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DataType {
|
||||||
|
Command,
|
||||||
|
Response,
|
||||||
|
Packet,
|
||||||
|
KeepAlive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DataType> for u32 {
|
||||||
|
fn from(value: DataType) -> Self {
|
||||||
|
match value {
|
||||||
|
DataType::Command => 1,
|
||||||
|
DataType::Response => 2,
|
||||||
|
DataType::Packet => 3,
|
||||||
|
DataType::KeepAlive => 0xCAFEBEEF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for DataType {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
1 => Self::Command,
|
||||||
|
2 => Self::Response,
|
||||||
|
3 => Self::Packet,
|
||||||
|
0xCAFEBEEF => Self::KeepAlive,
|
||||||
|
_ => return Err(Error::new("Unknown data type")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Command<'a> {
|
||||||
|
pub id: u8,
|
||||||
|
pub args: [u32; 2],
|
||||||
|
pub data: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Response {
|
||||||
|
pub id: u8,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Packet {
|
||||||
|
pub id: u8,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Backend {
|
||||||
|
fn send_command(&mut self, command: &Command) -> Result<(), Error>;
|
||||||
|
fn process_incoming_data(
|
||||||
|
&mut self,
|
||||||
|
data_type: DataType,
|
||||||
|
packets: &mut VecDeque<Packet>,
|
||||||
|
) -> Result<Option<Response>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SerialBackend {
|
||||||
|
serial: Box<dyn serialport::SerialPort>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerialBackend {
|
||||||
|
fn reset(&mut self) -> Result<(), Error> {
|
||||||
|
const WAIT_DURATION: Duration = Duration::from_millis(10);
|
||||||
|
const RETRY_COUNT: i32 = 100;
|
||||||
|
|
||||||
|
self.serial.write_data_terminal_ready(true)?;
|
||||||
|
for n in 0..=RETRY_COUNT {
|
||||||
|
self.serial.clear(serialport::ClearBuffer::All)?;
|
||||||
|
std::thread::sleep(WAIT_DURATION);
|
||||||
|
if self.serial.read_data_set_ready()? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if n == RETRY_COUNT {
|
||||||
|
return Err(Error::new("Couldn't reset SC64 device (on)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.serial.write_data_terminal_ready(false)?;
|
||||||
|
for n in 0..=RETRY_COUNT {
|
||||||
|
std::thread::sleep(WAIT_DURATION);
|
||||||
|
if !self.serial.read_data_set_ready()? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if n == RETRY_COUNT {
|
||||||
|
return Err(Error::new("Couldn't reset SC64 device (off)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend for SerialBackend {
|
||||||
|
fn send_command(&mut self, command: &Command) -> Result<(), Error> {
|
||||||
|
self.serial.write_all(b"CMD")?;
|
||||||
|
self.serial.write_all(&command.id.to_be_bytes())?;
|
||||||
|
self.serial.write_all(&command.args[0].to_be_bytes())?;
|
||||||
|
self.serial.write_all(&command.args[1].to_be_bytes())?;
|
||||||
|
|
||||||
|
self.serial.write_all(&command.data)?;
|
||||||
|
|
||||||
|
self.serial.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_incoming_data(
|
||||||
|
&mut self,
|
||||||
|
data_type: DataType,
|
||||||
|
packets: &mut VecDeque<Packet>,
|
||||||
|
) -> Result<Option<Response>, Error> {
|
||||||
|
let mut buffer = [0u8; 4];
|
||||||
|
|
||||||
|
while matches!(data_type, DataType::Response)
|
||||||
|
|| self.serial.bytes_to_read()? as usize >= buffer.len()
|
||||||
|
{
|
||||||
|
self.serial.read_exact(&mut buffer)?;
|
||||||
|
let (packet_token, error) = (match &buffer[0..3] {
|
||||||
|
b"CMP" => Ok((false, false)),
|
||||||
|
b"PKT" => Ok((true, false)),
|
||||||
|
b"ERR" => Ok((false, true)),
|
||||||
|
_ => Err(Error::new("Unknown response token")),
|
||||||
|
})?;
|
||||||
|
let id = buffer[3];
|
||||||
|
|
||||||
|
self.serial.read_exact(&mut buffer)?;
|
||||||
|
let length = u32::from_be_bytes(buffer) as usize;
|
||||||
|
|
||||||
|
let mut data = vec![0u8; length];
|
||||||
|
self.serial.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
if packet_token {
|
||||||
|
packets.push_back(Packet { id, data });
|
||||||
|
if matches!(data_type, DataType::Packet) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(Some(Response { id, error, data }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_serial_backend(port: &str) -> Result<SerialBackend, Error> {
|
||||||
|
let serial = serialport::new(port, 115_200)
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.open()?;
|
||||||
|
let mut backend = SerialBackend { serial };
|
||||||
|
backend.reset()?;
|
||||||
|
Ok(backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TcpBackend {
|
||||||
|
stream: TcpStream,
|
||||||
|
reader: BufReader<TcpStream>,
|
||||||
|
writer: BufWriter<TcpStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TcpBackend {
|
||||||
|
fn bytes_to_read(&mut self) -> Result<usize, Error> {
|
||||||
|
self.stream.set_nonblocking(true)?;
|
||||||
|
let result = self.reader.fill_buf();
|
||||||
|
let length = match result {
|
||||||
|
Ok(buffer) => buffer.len(),
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() == ErrorKind::WouldBlock {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.stream.set_nonblocking(false)?;
|
||||||
|
return Ok(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend for TcpBackend {
|
||||||
|
fn send_command(&mut self, command: &Command) -> Result<(), Error> {
|
||||||
|
let payload_data_type: u32 = DataType::Command.into();
|
||||||
|
self.writer.write_all(&payload_data_type.to_be_bytes())?;
|
||||||
|
|
||||||
|
self.writer.write_all(&command.id.to_be_bytes())?;
|
||||||
|
self.writer.write_all(&command.args[0].to_be_bytes())?;
|
||||||
|
self.writer.write_all(&command.args[1].to_be_bytes())?;
|
||||||
|
|
||||||
|
let command_data_length = command.data.len() as u32;
|
||||||
|
self.writer.write_all(&command_data_length.to_be_bytes())?;
|
||||||
|
self.writer.write_all(&command.data)?;
|
||||||
|
|
||||||
|
self.writer.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_incoming_data(
|
||||||
|
&mut self,
|
||||||
|
data_type: DataType,
|
||||||
|
packets: &mut VecDeque<Packet>,
|
||||||
|
) -> Result<Option<Response>, Error> {
|
||||||
|
let mut buffer = [0u8; 4];
|
||||||
|
|
||||||
|
while matches!(data_type, DataType::Response) || self.bytes_to_read()? >= 4 {
|
||||||
|
self.reader.read_exact(&mut buffer)?;
|
||||||
|
let payload_data_type: DataType = u32::from_be_bytes(buffer).try_into()?;
|
||||||
|
|
||||||
|
match payload_data_type {
|
||||||
|
DataType::Response => {
|
||||||
|
let mut response_info = vec![0u8; 2];
|
||||||
|
self.reader.read_exact(&mut response_info)?;
|
||||||
|
|
||||||
|
self.reader.read_exact(&mut buffer)?;
|
||||||
|
let response_data_length = u32::from_be_bytes(buffer) as usize;
|
||||||
|
|
||||||
|
let mut data = vec![0u8; response_data_length];
|
||||||
|
self.reader.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
return Ok(Some(Response {
|
||||||
|
id: response_info[0],
|
||||||
|
error: response_info[1] != 0,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
DataType::Packet => {
|
||||||
|
let mut packet_info = vec![0u8; 1];
|
||||||
|
self.reader.read_exact(&mut packet_info)?;
|
||||||
|
|
||||||
|
self.reader.read_exact(&mut buffer)?;
|
||||||
|
let packet_data_length = u32::from_be_bytes(buffer) as usize;
|
||||||
|
|
||||||
|
let mut data = vec![0u8; packet_data_length];
|
||||||
|
self.reader.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
packets.push_back(Packet {
|
||||||
|
id: packet_info[0],
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if matches!(data_type, DataType::Packet) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::KeepAlive => {}
|
||||||
|
_ => return Err(Error::new("Unexpected payload data type received")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_tcp_backend(address: &str) -> Result<TcpBackend, Error> {
|
||||||
|
let stream = match TcpStream::connect(address) {
|
||||||
|
Ok(stream) => {
|
||||||
|
stream.set_write_timeout(Some(Duration::from_secs(10)))?;
|
||||||
|
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!("Couldn't connect to [{address}]: {error}").as_str(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let reader = BufReader::new(stream.try_clone()?);
|
||||||
|
let writer = BufWriter::new(stream.try_clone()?);
|
||||||
|
Ok(TcpBackend {
|
||||||
|
stream,
|
||||||
|
reader,
|
||||||
|
writer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Link {
|
||||||
|
backend: Box<dyn Backend>,
|
||||||
|
packets: VecDeque<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Link {
|
||||||
|
pub fn execute_command(&mut self, command: &Command) -> Result<Vec<u8>, Error> {
|
||||||
|
self.execute_command_raw(command, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_command_raw(
|
||||||
|
&mut self,
|
||||||
|
command: &Command,
|
||||||
|
no_response: bool,
|
||||||
|
ignore_error: bool,
|
||||||
|
) -> Result<Vec<u8>, Error> {
|
||||||
|
self.backend.send_command(command)?;
|
||||||
|
if no_response {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let response = self.receive_response()?;
|
||||||
|
if command.id != response.id {
|
||||||
|
return Err(Error::new("Command response ID didn't match"));
|
||||||
|
}
|
||||||
|
if !ignore_error && response.error {
|
||||||
|
return Err(Error::new("Command response error"));
|
||||||
|
}
|
||||||
|
Ok(response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_response(&mut self) -> Result<Response, Error> {
|
||||||
|
match self
|
||||||
|
.backend
|
||||||
|
.process_incoming_data(DataType::Response, &mut self.packets)
|
||||||
|
{
|
||||||
|
Ok(response) => match response {
|
||||||
|
Some(response) => Ok(response),
|
||||||
|
None => Err(Error::new("No response was received")),
|
||||||
|
},
|
||||||
|
Err(error) => Err(Error::new(
|
||||||
|
format!("Command response error: {error}").as_str(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive_packet(&mut self) -> Result<Option<Packet>, Error> {
|
||||||
|
if self.packets.len() == 0 {
|
||||||
|
let response = self
|
||||||
|
.backend
|
||||||
|
.process_incoming_data(DataType::Packet, &mut self.packets)?;
|
||||||
|
if response.is_some() {
|
||||||
|
return Err(Error::new("Unexpected command response in data stream"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self.packets.pop_front())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_local(port: &str) -> Result<Link, Error> {
|
||||||
|
Ok(Link {
|
||||||
|
backend: Box::new(new_serial_backend(port)?),
|
||||||
|
packets: VecDeque::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_remote(address: &str) -> Result<Link, Error> {
|
||||||
|
Ok(Link {
|
||||||
|
backend: Box::new(new_tcp_backend(address)?),
|
||||||
|
packets: VecDeque::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocalDevice {
|
||||||
|
pub port: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_local_devices() -> Result<Vec<LocalDevice>, Error> {
|
||||||
|
const SC64_VID: u16 = 0x0403;
|
||||||
|
const SC64_PID: u16 = 0x6014;
|
||||||
|
const SC64_SID: &str = "SC64";
|
||||||
|
|
||||||
|
let mut serial_devices: Vec<LocalDevice> = Vec::new();
|
||||||
|
|
||||||
|
for device in serialport::available_ports()?.into_iter() {
|
||||||
|
if let serialport::SerialPortType::UsbPort(info) = device.port_type {
|
||||||
|
let serial_number = info.serial_number.unwrap_or("".to_string());
|
||||||
|
if info.vid == SC64_VID && info.pid == SC64_PID && serial_number.starts_with(SC64_SID) {
|
||||||
|
serial_devices.push(LocalDevice {
|
||||||
|
port: device.port_name,
|
||||||
|
serial_number,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serial_devices.len() == 0 {
|
||||||
|
return Err(Error::new("No SC64 devices found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(serial_devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ServerEvent {
|
||||||
|
Listening(String),
|
||||||
|
Connection(String),
|
||||||
|
Disconnected(String),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_server(
|
||||||
|
port: &str,
|
||||||
|
address: String,
|
||||||
|
event_callback: fn(ServerEvent),
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let listener = TcpListener::bind(address)?;
|
||||||
|
|
||||||
|
event_callback(ServerEvent::Listening(listener.local_addr()?.to_string()));
|
||||||
|
|
||||||
|
for stream in listener.incoming() {
|
||||||
|
match stream {
|
||||||
|
Ok(mut stream) => match server_accept_connection(port, event_callback, &mut stream) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => event_callback(ServerEvent::Err(error.to_string())),
|
||||||
|
},
|
||||||
|
Err(error) => return Err(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_accept_connection(
|
||||||
|
port: &str,
|
||||||
|
event_callback: fn(ServerEvent),
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let peer = stream.peer_addr()?.to_string();
|
||||||
|
|
||||||
|
stream.set_write_timeout(Some(Duration::from_secs(10)))?;
|
||||||
|
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(stream.try_clone()?);
|
||||||
|
let mut writer = BufWriter::new(stream.try_clone()?);
|
||||||
|
|
||||||
|
let mut serial_backend = new_serial_backend(port)?;
|
||||||
|
serial_backend.reset()?;
|
||||||
|
|
||||||
|
let mut packets: VecDeque<Packet> = VecDeque::new();
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 4];
|
||||||
|
|
||||||
|
let mut keepalive = Instant::now();
|
||||||
|
|
||||||
|
event_callback(ServerEvent::Connection(peer.clone()));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
stream.set_nonblocking(true)?;
|
||||||
|
match reader.read_exact(&mut buffer) {
|
||||||
|
Ok(()) => {
|
||||||
|
stream.set_nonblocking(false)?;
|
||||||
|
|
||||||
|
let data_type: DataType = u32::from_be_bytes(buffer).try_into()?;
|
||||||
|
|
||||||
|
if !matches!(data_type, DataType::Command) {
|
||||||
|
return Err(Error::new("Received data type wasn't a command data type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut id_buffer = [0u8; 1];
|
||||||
|
let mut args = [0u32; 2];
|
||||||
|
|
||||||
|
reader.read_exact(&mut id_buffer)?;
|
||||||
|
reader.read_exact(&mut buffer)?;
|
||||||
|
args[0] = u32::from_be_bytes(buffer);
|
||||||
|
reader.read_exact(&mut buffer)?;
|
||||||
|
args[1] = u32::from_be_bytes(buffer);
|
||||||
|
|
||||||
|
reader.read_exact(&mut buffer)?;
|
||||||
|
let command_data_length = u32::from_be_bytes(buffer) as usize;
|
||||||
|
let mut data = vec![0u8; command_data_length];
|
||||||
|
reader.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
serial_backend.send_command(&Command {
|
||||||
|
id: id_buffer[0],
|
||||||
|
args,
|
||||||
|
data: &data,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() != ErrorKind::WouldBlock {
|
||||||
|
event_callback(ServerEvent::Disconnected(peer.clone()));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
stream.set_nonblocking(false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(response) =
|
||||||
|
serial_backend.process_incoming_data(DataType::Packet, &mut packets)?
|
||||||
|
{
|
||||||
|
writer.write_all(&u32::to_be_bytes(DataType::Response.into()))?;
|
||||||
|
writer.write_all(&[response.id])?;
|
||||||
|
writer.write_all(&[response.error as u8])?;
|
||||||
|
writer.write_all(&(response.data.len() as u32).to_be_bytes())?;
|
||||||
|
writer.write_all(&response.data)?;
|
||||||
|
writer.flush()?;
|
||||||
|
} else if let Some(packet) = packets.pop_front() {
|
||||||
|
writer.write_all(&u32::to_be_bytes(DataType::Packet.into()))?;
|
||||||
|
writer.write_all(&[packet.id])?;
|
||||||
|
writer.write_all(&(packet.data.len() as u32).to_be_bytes())?;
|
||||||
|
writer.write_all(&packet.data)?;
|
||||||
|
writer.flush()?;
|
||||||
|
} else if keepalive.elapsed() > Duration::from_secs(5) {
|
||||||
|
keepalive = Instant::now();
|
||||||
|
writer.write_all(&u32::to_be_bytes(DataType::KeepAlive.into()))?;
|
||||||
|
writer.flush()?;
|
||||||
|
} else {
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
788
sw/deployer/src/sc64/mod.rs
Normal file
788
sw/deployer/src/sc64/mod.rs
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
mod cic;
|
||||||
|
mod error;
|
||||||
|
pub mod firmware;
|
||||||
|
mod link;
|
||||||
|
mod types;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
error::Error,
|
||||||
|
link::{list_local_devices, ServerEvent},
|
||||||
|
types::{
|
||||||
|
BootMode, ButtonMode, ButtonState, CicSeed, DataPacket, DdDiskState, DdDriveType, DdMode,
|
||||||
|
DebugPacket, DiskPacket, DiskPacketKind, FpgaDebugData, McuStackUsage, SaveType, Switch,
|
||||||
|
TvType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
cic::{calculate_ipl3_checksum, guess_ipl3_seed, IPL3_LENGTH, IPL3_OFFSET},
|
||||||
|
link::{Command, Link},
|
||||||
|
types::{
|
||||||
|
get_config, get_setting, Config, ConfigId, FirmwareStatus, Setting, SettingId, UpdateStatus,
|
||||||
|
},
|
||||||
|
utils::{convert_from_datetime, convert_to_datetime},
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use std::{
|
||||||
|
io::{Read, Seek, Write},
|
||||||
|
time::Instant,
|
||||||
|
{cmp::min, time::Duration},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SC64 {
|
||||||
|
link: Link,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeviceState {
|
||||||
|
pub bootloader_switch: Switch,
|
||||||
|
pub rom_write_enable: Switch,
|
||||||
|
pub rom_shadow_enable: Switch,
|
||||||
|
pub dd_mode: DdMode,
|
||||||
|
pub isv_address: u32,
|
||||||
|
pub boot_mode: BootMode,
|
||||||
|
pub save_type: SaveType,
|
||||||
|
pub cic_seed: CicSeed,
|
||||||
|
pub tv_type: TvType,
|
||||||
|
pub dd_sd_enable: Switch,
|
||||||
|
pub dd_drive_type: DdDriveType,
|
||||||
|
pub dd_disk_state: DdDiskState,
|
||||||
|
pub button_state: ButtonState,
|
||||||
|
pub button_mode: ButtonMode,
|
||||||
|
pub rom_extended_enable: Switch,
|
||||||
|
pub led_enable: Switch,
|
||||||
|
pub datetime: DateTime<Local>,
|
||||||
|
pub fpga_debug_data: FpgaDebugData,
|
||||||
|
pub mcu_stack_usage: McuStackUsage,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SC64_V2_IDENTIFIER: &[u8; 4] = b"SCv2";
|
||||||
|
|
||||||
|
const SUPPORTED_MAJOR_VERSION: u16 = 2;
|
||||||
|
const SUPPORTED_MINOR_VERSION: u16 = 12;
|
||||||
|
|
||||||
|
const SDRAM_ADDRESS: u32 = 0x0000_0000;
|
||||||
|
const SDRAM_LENGTH: usize = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
const ROM_SHADOW_ADDRESS: u32 = 0x04FE_0000;
|
||||||
|
const ROM_SHADOW_LENGTH: usize = 128 * 1024;
|
||||||
|
|
||||||
|
const ROM_EXTENDED_ADDRESS: u32 = 0x0400_0000;
|
||||||
|
const ROM_EXTENDED_LENGTH: usize = 14 * 1024 * 1024;
|
||||||
|
|
||||||
|
const MAX_ROM_LENGTH: usize = 78 * 1024 * 1024;
|
||||||
|
|
||||||
|
const DDIPL_ADDRESS: u32 = 0x03BC_0000;
|
||||||
|
const DDIPL_LENGTH: usize = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
const SAVE_ADDRESS: u32 = 0x03FE_0000;
|
||||||
|
const EEPROM_ADDRESS: u32 = 0x0500_2000;
|
||||||
|
|
||||||
|
const EEPROM_4K_LENGTH: usize = 512;
|
||||||
|
const EEPROM_16K_LENGTH: usize = 2 * 1024;
|
||||||
|
const SRAM_LENGTH: usize = 32 * 1024;
|
||||||
|
const SRAM_BANKED_LENGTH: usize = 3 * 32 * 1024;
|
||||||
|
const FLASHRAM_LENGTH: usize = 128 * 1024;
|
||||||
|
|
||||||
|
const BOOTLOADER_ADDRESS: u32 = 0x04E0_0000;
|
||||||
|
|
||||||
|
const FIRMWARE_ADDRESS: u32 = 0x0010_0000; // Arbitrary offset in SDRAM memory
|
||||||
|
const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90);
|
||||||
|
|
||||||
|
const ISV_BUFFER_LENGTH: usize = 64 * 1024;
|
||||||
|
|
||||||
|
pub const MEMORY_LENGTH: usize = 0x0500_2980;
|
||||||
|
|
||||||
|
const MEMORY_CHUNK_LENGTH: usize = 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
impl SC64 {
|
||||||
|
fn command_identifier_get(&mut self) -> Result<[u8; 4], Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'v',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for identifier get command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(data[0..4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_version_get(&mut self) -> Result<(u16, u16, u32), Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'V',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 8 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for version get command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let major = u16::from_be_bytes(data[0..2].try_into().unwrap());
|
||||||
|
let minor = u16::from_be_bytes(data[2..4].try_into().unwrap());
|
||||||
|
let revision = u32::from_be_bytes(data[4..8].try_into().unwrap());
|
||||||
|
Ok((major, minor, revision))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_state_reset(&mut self) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'R',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_cic_params_set(
|
||||||
|
&mut self,
|
||||||
|
disable: bool,
|
||||||
|
seed: u8,
|
||||||
|
checksum: &[u8; 6],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let args = [
|
||||||
|
u32::from_be_bytes([(disable as u8) << 0, seed, checksum[0], checksum[1]]),
|
||||||
|
u32::from_be_bytes([checksum[2], checksum[3], checksum[4], checksum[5]]),
|
||||||
|
];
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'B',
|
||||||
|
args,
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_config_get(&mut self, config_id: ConfigId) -> Result<Config, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'c',
|
||||||
|
args: [config_id.into(), 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for config get command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let value = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||||
|
Ok((config_id, value).try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_config_set(&mut self, config: Config) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'C',
|
||||||
|
args: config.into(),
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_setting_get(&mut self, setting_id: SettingId) -> Result<Setting, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'a',
|
||||||
|
args: [setting_id.into(), 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for setting get command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let value = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||||
|
Ok((setting_id, value).try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_setting_set(&mut self, setting: Setting) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'A',
|
||||||
|
args: setting.into(),
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_time_get(&mut self) -> Result<DateTime<Local>, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b't',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 8 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for time get command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(convert_to_datetime(&data[0..8].try_into().unwrap())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_time_set(&mut self, datetime: DateTime<Local>) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'T',
|
||||||
|
args: convert_from_datetime(datetime),
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_memory_read(&mut self, address: u32, length: usize) -> Result<Vec<u8>, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'm',
|
||||||
|
args: [address, length as u32],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != length {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for memory read command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_memory_write(&mut self, address: u32, data: &[u8]) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'M',
|
||||||
|
args: [address, data.len() as u32],
|
||||||
|
data,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_usb_write(&mut self, datatype: u8, data: &[u8]) -> Result<(), Error> {
|
||||||
|
self.link.execute_command_raw(
|
||||||
|
&Command {
|
||||||
|
id: b'U',
|
||||||
|
args: [datatype as u32, data.len() as u32],
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'D',
|
||||||
|
args: [error as u32, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_flash_wait_busy(&mut self, wait: bool) -> Result<u32, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'p',
|
||||||
|
args: [wait as u32, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
if data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for flash wait busy command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let erase_block_size = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||||
|
Ok(erase_block_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_flash_erase_block(&mut self, address: u32) -> Result<(), Error> {
|
||||||
|
self.link.execute_command(&Command {
|
||||||
|
id: b'P',
|
||||||
|
args: [address, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_firmware_backup(&mut self, address: u32) -> Result<(FirmwareStatus, u32), Error> {
|
||||||
|
let data = self.link.execute_command_raw(
|
||||||
|
&Command {
|
||||||
|
id: b'f',
|
||||||
|
args: [address, 0],
|
||||||
|
data: &[],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
if data.len() != 8 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for firmware backup command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let status = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||||
|
let length = u32::from_be_bytes(data[4..8].try_into().unwrap());
|
||||||
|
Ok((status.try_into()?, length))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_firmware_update(
|
||||||
|
&mut self,
|
||||||
|
address: u32,
|
||||||
|
length: usize,
|
||||||
|
) -> Result<FirmwareStatus, Error> {
|
||||||
|
let data = self.link.execute_command_raw(
|
||||||
|
&Command {
|
||||||
|
id: b'F',
|
||||||
|
args: [address, length as u32],
|
||||||
|
data: &[],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
if data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Invalid data length received for firmware update command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(u32::from_be_bytes(data[0..4].try_into().unwrap()).try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_debug_get(&mut self) -> Result<FpgaDebugData, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'?',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(data.try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_stack_usage_get(&mut self) -> Result<McuStackUsage, Error> {
|
||||||
|
let data = self.link.execute_command(&Command {
|
||||||
|
id: b'%',
|
||||||
|
args: [0, 0],
|
||||||
|
data: &[],
|
||||||
|
})?;
|
||||||
|
Ok(data.try_into()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SC64 {
|
||||||
|
pub fn upload_rom<T: Read + Seek>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut T,
|
||||||
|
length: usize,
|
||||||
|
no_shadow: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if length > MAX_ROM_LENGTH {
|
||||||
|
return Err(Error::new("ROM length too big"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pi_config = vec![0u8; 4];
|
||||||
|
|
||||||
|
reader.rewind()?;
|
||||||
|
reader.read_exact(&mut pi_config)?;
|
||||||
|
reader.rewind()?;
|
||||||
|
|
||||||
|
let endian_swapper = match &pi_config[0..4] {
|
||||||
|
[0x37, 0x80, 0x40, 0x12] => {
|
||||||
|
|b: &mut [u8]| b.chunks_exact_mut(2).for_each(|c| c.swap(0, 1))
|
||||||
|
}
|
||||||
|
[0x40, 0x12, 0x37, 0x80] => |b: &mut [u8]| {
|
||||||
|
b.chunks_exact_mut(4).for_each(|c| {
|
||||||
|
c.swap(0, 3);
|
||||||
|
c.swap(1, 2)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_ => |_: &mut [u8]| {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let rom_shadow_enabled = !no_shadow && length > (SDRAM_LENGTH - ROM_SHADOW_LENGTH);
|
||||||
|
let rom_extended_enabled = length > SDRAM_LENGTH;
|
||||||
|
|
||||||
|
let sdram_length = if rom_shadow_enabled {
|
||||||
|
min(length, SDRAM_LENGTH - ROM_SHADOW_LENGTH)
|
||||||
|
} else {
|
||||||
|
min(length, SDRAM_LENGTH)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.memory_write_chunked(reader, SDRAM_ADDRESS, sdram_length, Some(endian_swapper))?;
|
||||||
|
|
||||||
|
self.command_config_set(Config::RomShadowEnable(rom_shadow_enabled.into()))?;
|
||||||
|
if rom_shadow_enabled {
|
||||||
|
let rom_shadow_length = min(length - sdram_length, ROM_SHADOW_LENGTH);
|
||||||
|
self.flash_program(
|
||||||
|
reader,
|
||||||
|
ROM_SHADOW_ADDRESS,
|
||||||
|
rom_shadow_length,
|
||||||
|
Some(endian_swapper),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_config_set(Config::RomExtendedEnable(rom_extended_enabled.into()))?;
|
||||||
|
if rom_extended_enabled {
|
||||||
|
let rom_extended_length = min(length - SDRAM_LENGTH, ROM_EXTENDED_LENGTH);
|
||||||
|
self.flash_program(
|
||||||
|
reader,
|
||||||
|
ROM_EXTENDED_ADDRESS,
|
||||||
|
rom_extended_length,
|
||||||
|
Some(endian_swapper),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload_ddipl<T: Read>(&mut self, reader: &mut T, length: usize) -> Result<(), Error> {
|
||||||
|
if length > DDIPL_LENGTH {
|
||||||
|
return Err(Error::new("DDIPL length too big"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.memory_write_chunked(reader, DDIPL_ADDRESS, length, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload_save<T: Read>(&mut self, reader: &mut T, length: usize) -> Result<(), Error> {
|
||||||
|
let save_type = get_config!(self, SaveType)?;
|
||||||
|
|
||||||
|
let (address, save_length) = match save_type {
|
||||||
|
SaveType::None => {
|
||||||
|
return Err(Error::new("No save type is enabled"));
|
||||||
|
}
|
||||||
|
SaveType::Eeprom4k => (EEPROM_ADDRESS, EEPROM_4K_LENGTH),
|
||||||
|
SaveType::Eeprom16k => (EEPROM_ADDRESS, EEPROM_16K_LENGTH),
|
||||||
|
SaveType::Sram => (SAVE_ADDRESS, SRAM_LENGTH),
|
||||||
|
SaveType::SramBanked => (SAVE_ADDRESS, SRAM_BANKED_LENGTH),
|
||||||
|
SaveType::Flashram => (SAVE_ADDRESS, FLASHRAM_LENGTH),
|
||||||
|
};
|
||||||
|
|
||||||
|
if length != save_length {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Save file size did not match currently enabled save type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.memory_write_chunked(reader, address, save_length, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_save<T: Write>(&mut self, writer: &mut T) -> Result<(), Error> {
|
||||||
|
let save_type = get_config!(self, SaveType)?;
|
||||||
|
|
||||||
|
let (address, save_length) = match save_type {
|
||||||
|
SaveType::None => {
|
||||||
|
return Err(Error::new("No save type is enabled"));
|
||||||
|
}
|
||||||
|
SaveType::Eeprom4k => (EEPROM_ADDRESS, EEPROM_4K_LENGTH),
|
||||||
|
SaveType::Eeprom16k => (EEPROM_ADDRESS, EEPROM_16K_LENGTH),
|
||||||
|
SaveType::Sram => (SAVE_ADDRESS, SRAM_LENGTH),
|
||||||
|
SaveType::SramBanked => (SAVE_ADDRESS, SRAM_BANKED_LENGTH),
|
||||||
|
SaveType::Flashram => (SAVE_ADDRESS, FLASHRAM_LENGTH),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.memory_read_chunked(writer, address, save_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_memory<T: Write>(
|
||||||
|
&mut self,
|
||||||
|
writer: &mut T,
|
||||||
|
address: u32,
|
||||||
|
length: usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if address + length as u32 > MEMORY_LENGTH as u32 {
|
||||||
|
return Err(Error::new("Invalid dump address or length"));
|
||||||
|
}
|
||||||
|
self.memory_read_chunked(writer, address, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_cic_parameters(&mut self) -> Result<(), Error> {
|
||||||
|
let boot_mode = get_config!(self, BootMode)?;
|
||||||
|
let address = match boot_mode {
|
||||||
|
BootMode::DirectRom => SDRAM_ADDRESS,
|
||||||
|
BootMode::DirectDdIpl => DDIPL_ADDRESS,
|
||||||
|
_ => BOOTLOADER_ADDRESS,
|
||||||
|
};
|
||||||
|
let ipl3 = self.command_memory_read(address + IPL3_OFFSET, IPL3_LENGTH)?;
|
||||||
|
let seed = guess_ipl3_seed(&ipl3)?;
|
||||||
|
let checksum = &calculate_ipl3_checksum(&ipl3, seed)?;
|
||||||
|
self.command_cic_params_set(false, seed, checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_boot_mode(&mut self, boot_mode: BootMode) -> Result<(), Error> {
|
||||||
|
self.command_config_set(Config::BootMode(boot_mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_save_type(&mut self, save_type: SaveType) -> Result<(), Error> {
|
||||||
|
self.command_config_set(Config::SaveType(save_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_tv_type(&mut self, tv_type: TvType) -> Result<(), Error> {
|
||||||
|
self.command_config_set(Config::TvType(tv_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_datetime(&mut self) -> Result<DateTime<Local>, Error> {
|
||||||
|
self.command_time_get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_datetime(&mut self, datetime: DateTime<Local>) -> Result<(), Error> {
|
||||||
|
self.command_time_set(datetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_led_blink(&mut self, enabled: bool) -> Result<(), Error> {
|
||||||
|
self.command_setting_set(Setting::LedEnable(enabled.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_state(&mut self) -> Result<DeviceState, Error> {
|
||||||
|
Ok(DeviceState {
|
||||||
|
bootloader_switch: get_config!(self, BootloaderSwitch)?,
|
||||||
|
rom_write_enable: get_config!(self, RomWriteEnable)?,
|
||||||
|
rom_shadow_enable: get_config!(self, RomShadowEnable)?,
|
||||||
|
dd_mode: get_config!(self, DdMode)?,
|
||||||
|
isv_address: get_config!(self, IsvAddress)?,
|
||||||
|
boot_mode: get_config!(self, BootMode)?,
|
||||||
|
save_type: get_config!(self, SaveType)?,
|
||||||
|
cic_seed: get_config!(self, CicSeed)?,
|
||||||
|
tv_type: get_config!(self, TvType)?,
|
||||||
|
dd_sd_enable: get_config!(self, DdSdEnable)?,
|
||||||
|
dd_drive_type: get_config!(self, DdDriveType)?,
|
||||||
|
dd_disk_state: get_config!(self, DdDiskState)?,
|
||||||
|
button_state: get_config!(self, ButtonState)?,
|
||||||
|
button_mode: get_config!(self, ButtonMode)?,
|
||||||
|
rom_extended_enable: get_config!(self, RomExtendedEnable)?,
|
||||||
|
led_enable: get_setting!(self, LedEnable)?,
|
||||||
|
datetime: self.get_datetime()?,
|
||||||
|
fpga_debug_data: self.command_debug_get()?,
|
||||||
|
mcu_stack_usage: self.command_stack_usage_get()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure_64dd(
|
||||||
|
&mut self,
|
||||||
|
dd_mode: DdMode,
|
||||||
|
drive_type: DdDriveType,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.command_config_set(Config::DdMode(dd_mode))?;
|
||||||
|
self.command_config_set(Config::DdSdEnable(Switch::Off))?;
|
||||||
|
self.command_config_set(Config::DdDriveType(drive_type))?;
|
||||||
|
self.command_config_set(Config::DdDiskState(DdDiskState::Ejected))?;
|
||||||
|
self.command_config_set(Config::ButtonMode(ButtonMode::UsbPacket))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_64dd_disk_state(&mut self, disk_state: DdDiskState) -> Result<(), Error> {
|
||||||
|
self.command_config_set(Config::DdDiskState(disk_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure_is_viewer_64(&mut self, offset: Option<u32>) -> Result<(), Error> {
|
||||||
|
if let Some(offset) = offset {
|
||||||
|
if get_config!(self, RomShadowEnable)?.into() {
|
||||||
|
if offset > (SAVE_ADDRESS - ISV_BUFFER_LENGTH as u32) {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!(
|
||||||
|
"ROM shadow is enabled, IS-Viewer 64 at offset 0x{offset:08X} won't work"
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.command_config_set(Config::RomWriteEnable(Switch::On))?;
|
||||||
|
self.command_config_set(Config::IsvAddress(offset))?;
|
||||||
|
} else {
|
||||||
|
self.command_config_set(Config::RomWriteEnable(Switch::Off))?;
|
||||||
|
self.command_config_set(Config::IsvAddress(0))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive_data_packet(&mut self) -> Result<Option<DataPacket>, Error> {
|
||||||
|
if let Some(packet) = self.link.receive_packet()? {
|
||||||
|
return Ok(Some(packet.try_into()?));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reply_disk_packet(&mut self, disk_packet: Option<DiskPacket>) -> Result<(), Error> {
|
||||||
|
if let Some(packet) = disk_packet {
|
||||||
|
match packet.kind {
|
||||||
|
DiskPacketKind::Read => {
|
||||||
|
self.command_memory_write(packet.info.address, &packet.info.data)?;
|
||||||
|
}
|
||||||
|
DiskPacketKind::Write => {}
|
||||||
|
}
|
||||||
|
self.command_dd_set_block_ready(false)?;
|
||||||
|
} else {
|
||||||
|
self.command_dd_set_block_ready(true)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_debug_packet(&mut self, debug_packet: DebugPacket) -> Result<(), Error> {
|
||||||
|
self.command_usb_write(debug_packet.datatype, &debug_packet.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
})?;
|
||||||
|
if &identifier != SC64_V2_IDENTIFIER {
|
||||||
|
return Err(Error::new("Unknown identifier received, not a SC64 device"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_firmware_version(&mut self) -> Result<(u16, u16, u32), Error> {
|
||||||
|
let (major, minor, revision) = self
|
||||||
|
.command_version_get()
|
||||||
|
.map_err(|_| Error::new("Outdated SC64 firmware version, please update firmware"))?;
|
||||||
|
if major != SUPPORTED_MAJOR_VERSION || minor < SUPPORTED_MINOR_VERSION {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Unsupported SC64 firmware version, please update firmware",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok((major, minor, revision))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_state(&mut self) -> Result<(), Error> {
|
||||||
|
self.command_state_reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backup_firmware(&mut self) -> Result<Vec<u8>, Error> {
|
||||||
|
self.command_state_reset()?;
|
||||||
|
let (status, length) = self.command_firmware_backup(FIRMWARE_ADDRESS)?;
|
||||||
|
if !matches!(status, FirmwareStatus::Ok) {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!("Firmware backup error: {}", status).as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.command_memory_read(FIRMWARE_ADDRESS, length as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_firmware(&mut self, data: &[u8]) -> Result<(), Error> {
|
||||||
|
self.command_state_reset()?;
|
||||||
|
self.command_memory_write(FIRMWARE_ADDRESS, data)?;
|
||||||
|
let status = self.command_firmware_update(FIRMWARE_ADDRESS, data.len())?;
|
||||||
|
if !matches!(status, FirmwareStatus::Ok) {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!("Firmware update verify error: {}", status).as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let timeout = Instant::now();
|
||||||
|
let mut last_update_status = UpdateStatus::Err;
|
||||||
|
loop {
|
||||||
|
if let Some(packet) = self.receive_data_packet()? {
|
||||||
|
if let DataPacket::UpdateStatus(status) = packet {
|
||||||
|
match status {
|
||||||
|
UpdateStatus::Done => {
|
||||||
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
UpdateStatus::Err => {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!(
|
||||||
|
"Firmware update error on step {}, device is, most likely, bricked",
|
||||||
|
last_update_status
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
current_update_status => last_update_status = current_update_status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if timeout.elapsed() > FIRMWARE_UPDATE_TIMEOUT {
|
||||||
|
return Err(Error::new(
|
||||||
|
format!(
|
||||||
|
"Firmware update timeout, SC64 did not finish update in {} seconds, last step: {}",
|
||||||
|
FIRMWARE_UPDATE_TIMEOUT.as_secs(),
|
||||||
|
last_update_status
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_read_chunked(
|
||||||
|
&mut self,
|
||||||
|
writer: &mut dyn Write,
|
||||||
|
address: u32,
|
||||||
|
length: usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut memory_address = address;
|
||||||
|
let mut bytes_left = length;
|
||||||
|
while bytes_left > 0 {
|
||||||
|
let bytes = min(MEMORY_CHUNK_LENGTH, bytes_left);
|
||||||
|
let data = self.command_memory_read(memory_address, bytes)?;
|
||||||
|
writer.write_all(&data)?;
|
||||||
|
memory_address += bytes as u32;
|
||||||
|
bytes_left -= bytes;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_write_chunked(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut dyn Read,
|
||||||
|
address: u32,
|
||||||
|
length: usize,
|
||||||
|
transform: Option<fn(&mut [u8])>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut limited_reader = reader.take(length as u64);
|
||||||
|
let mut memory_address = address;
|
||||||
|
let mut data: Vec<u8> = vec![0u8; MEMORY_CHUNK_LENGTH];
|
||||||
|
loop {
|
||||||
|
let bytes = limited_reader.read(&mut data)?;
|
||||||
|
if bytes == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(transform) = transform {
|
||||||
|
transform(&mut data[0..bytes]);
|
||||||
|
}
|
||||||
|
self.command_memory_write(memory_address, &data[0..bytes])?;
|
||||||
|
memory_address += bytes as u32;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flash_erase(&mut self, address: u32, length: usize) -> Result<(), Error> {
|
||||||
|
let erase_block_size = self.command_flash_wait_busy(false)?;
|
||||||
|
for offset in (0..length as u32).step_by(erase_block_size as usize) {
|
||||||
|
self.command_flash_erase_block(address + offset)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flash_program(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut dyn Read,
|
||||||
|
address: u32,
|
||||||
|
length: usize,
|
||||||
|
transform: Option<fn(&mut [u8])>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.flash_erase(address, length)?;
|
||||||
|
self.memory_write_chunked(reader, address, length, transform)?;
|
||||||
|
self.command_flash_wait_busy(true)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_local(port: Option<String>) -> Result<SC64, Error> {
|
||||||
|
let port = if let Some(port) = port {
|
||||||
|
port
|
||||||
|
} else {
|
||||||
|
list_local_devices()?[0].port.clone()
|
||||||
|
};
|
||||||
|
let mut sc64 = SC64 {
|
||||||
|
link: link::new_local(&port)?,
|
||||||
|
};
|
||||||
|
sc64.check_device()?;
|
||||||
|
Ok(sc64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_remote(address: String) -> Result<SC64, Error> {
|
||||||
|
let mut sc64 = SC64 {
|
||||||
|
link: link::new_remote(&address)?,
|
||||||
|
};
|
||||||
|
sc64.check_device()?;
|
||||||
|
Ok(sc64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_server(
|
||||||
|
port: Option<String>,
|
||||||
|
address: String,
|
||||||
|
event_callback: fn(ServerEvent),
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let port = if let Some(port) = port {
|
||||||
|
port
|
||||||
|
} else {
|
||||||
|
list_local_devices()?[0].port.clone()
|
||||||
|
};
|
||||||
|
link::run_server(&port, address, event_callback)
|
||||||
|
}
|
856
sw/deployer/src/sc64/types.rs
Normal file
856
sw/deployer/src/sc64/types.rs
Normal file
@ -0,0 +1,856 @@
|
|||||||
|
use super::{link::Packet, Error};
|
||||||
|
use encoding_rs::EUC_JP;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum ConfigId {
|
||||||
|
BootloaderSwitch,
|
||||||
|
RomWriteEnable,
|
||||||
|
RomShadowEnable,
|
||||||
|
DdMode,
|
||||||
|
IsvAddress,
|
||||||
|
BootMode,
|
||||||
|
SaveType,
|
||||||
|
CicSeed,
|
||||||
|
TvType,
|
||||||
|
DdSdEnable,
|
||||||
|
DdDriveType,
|
||||||
|
DdDiskState,
|
||||||
|
ButtonState,
|
||||||
|
ButtonMode,
|
||||||
|
RomExtendedEnable,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Config {
|
||||||
|
BootloaderSwitch(Switch),
|
||||||
|
RomWriteEnable(Switch),
|
||||||
|
RomShadowEnable(Switch),
|
||||||
|
DdMode(DdMode),
|
||||||
|
IsvAddress(u32),
|
||||||
|
BootMode(BootMode),
|
||||||
|
SaveType(SaveType),
|
||||||
|
CicSeed(CicSeed),
|
||||||
|
TvType(TvType),
|
||||||
|
DdSdEnable(Switch),
|
||||||
|
DdDriveType(DdDriveType),
|
||||||
|
DdDiskState(DdDiskState),
|
||||||
|
ButtonState(ButtonState),
|
||||||
|
ButtonMode(ButtonMode),
|
||||||
|
RomExtendedEnable(Switch),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ConfigId> for u32 {
|
||||||
|
fn from(value: ConfigId) -> Self {
|
||||||
|
match value {
|
||||||
|
ConfigId::BootloaderSwitch => 0,
|
||||||
|
ConfigId::RomWriteEnable => 1,
|
||||||
|
ConfigId::RomShadowEnable => 2,
|
||||||
|
ConfigId::DdMode => 3,
|
||||||
|
ConfigId::IsvAddress => 4,
|
||||||
|
ConfigId::BootMode => 5,
|
||||||
|
ConfigId::SaveType => 6,
|
||||||
|
ConfigId::CicSeed => 7,
|
||||||
|
ConfigId::TvType => 8,
|
||||||
|
ConfigId::DdSdEnable => 9,
|
||||||
|
ConfigId::DdDriveType => 10,
|
||||||
|
ConfigId::DdDiskState => 11,
|
||||||
|
ConfigId::ButtonState => 12,
|
||||||
|
ConfigId::ButtonMode => 13,
|
||||||
|
ConfigId::RomExtendedEnable => 14,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(ConfigId, u32)> for Config {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: (ConfigId, u32)) -> Result<Self, Self::Error> {
|
||||||
|
let (id, config) = value;
|
||||||
|
Ok(match id {
|
||||||
|
ConfigId::BootloaderSwitch => Self::BootloaderSwitch(config.try_into()?),
|
||||||
|
ConfigId::RomWriteEnable => Self::RomWriteEnable(config.try_into()?),
|
||||||
|
ConfigId::RomShadowEnable => Self::RomShadowEnable(config.try_into()?),
|
||||||
|
ConfigId::DdMode => Self::DdMode(config.try_into()?),
|
||||||
|
ConfigId::IsvAddress => Self::IsvAddress(config),
|
||||||
|
ConfigId::BootMode => Self::BootMode(config.try_into()?),
|
||||||
|
ConfigId::SaveType => Self::SaveType(config.try_into()?),
|
||||||
|
ConfigId::CicSeed => Self::CicSeed(config.try_into()?),
|
||||||
|
ConfigId::TvType => Self::TvType(config.try_into()?),
|
||||||
|
ConfigId::DdSdEnable => Self::DdSdEnable(config.try_into()?),
|
||||||
|
ConfigId::DdDriveType => Self::DdDriveType(config.try_into()?),
|
||||||
|
ConfigId::DdDiskState => Self::DdDiskState(config.try_into()?),
|
||||||
|
ConfigId::ButtonState => Self::ButtonState(config.try_into()?),
|
||||||
|
ConfigId::ButtonMode => Self::ButtonMode(config.try_into()?),
|
||||||
|
ConfigId::RomExtendedEnable => Self::RomExtendedEnable(config.try_into()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Config> for [u32; 2] {
|
||||||
|
fn from(value: Config) -> Self {
|
||||||
|
match value {
|
||||||
|
Config::BootloaderSwitch(val) => [ConfigId::BootloaderSwitch.into(), val.into()],
|
||||||
|
Config::RomWriteEnable(val) => [ConfigId::RomWriteEnable.into(), val.into()],
|
||||||
|
Config::RomShadowEnable(val) => [ConfigId::RomShadowEnable.into(), val.into()],
|
||||||
|
Config::DdMode(val) => [ConfigId::DdMode.into(), val.into()],
|
||||||
|
Config::IsvAddress(val) => [ConfigId::IsvAddress.into(), val.into()],
|
||||||
|
Config::BootMode(val) => [ConfigId::BootMode.into(), val.into()],
|
||||||
|
Config::SaveType(val) => [ConfigId::SaveType.into(), val.into()],
|
||||||
|
Config::CicSeed(val) => [ConfigId::CicSeed.into(), val.into()],
|
||||||
|
Config::TvType(val) => [ConfigId::TvType.into(), val.into()],
|
||||||
|
Config::DdSdEnable(val) => [ConfigId::DdSdEnable.into(), val.into()],
|
||||||
|
Config::DdDriveType(val) => [ConfigId::DdDriveType.into(), val.into()],
|
||||||
|
Config::DdDiskState(val) => [ConfigId::DdDiskState.into(), val.into()],
|
||||||
|
Config::ButtonState(val) => [ConfigId::ButtonState.into(), val.into()],
|
||||||
|
Config::ButtonMode(val) => [ConfigId::ButtonMode.into(), val.into()],
|
||||||
|
Config::RomExtendedEnable(val) => [ConfigId::RomExtendedEnable.into(), val.into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Switch {
|
||||||
|
Off,
|
||||||
|
On,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Switch {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Switch::Off => "Disabled",
|
||||||
|
Switch::On => "Enabled",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for Switch {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Off,
|
||||||
|
_ => Self::On,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Switch> for u32 {
|
||||||
|
fn from(value: Switch) -> Self {
|
||||||
|
match value {
|
||||||
|
Switch::Off => 0,
|
||||||
|
Switch::On => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for Switch {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
match value {
|
||||||
|
false => Self::Off,
|
||||||
|
true => Self::On,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Switch> for bool {
|
||||||
|
fn from(value: Switch) -> Self {
|
||||||
|
match value {
|
||||||
|
Switch::Off => false,
|
||||||
|
Switch::On => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DdMode {
|
||||||
|
None,
|
||||||
|
Regs,
|
||||||
|
DdIpl,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DdMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
DdMode::None => "Disabled",
|
||||||
|
DdMode::Regs => "Only registers",
|
||||||
|
DdMode::DdIpl => "Only 64DD IPL",
|
||||||
|
DdMode::Full => "Registers + 64DD IPL",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for DdMode {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::None,
|
||||||
|
1 => Self::Regs,
|
||||||
|
2 => Self::DdIpl,
|
||||||
|
3 => Self::Full,
|
||||||
|
_ => return Err(Error::new("Unknown 64DD mode code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DdMode> for u32 {
|
||||||
|
fn from(value: DdMode) -> Self {
|
||||||
|
match value {
|
||||||
|
DdMode::None => 0,
|
||||||
|
DdMode::Regs => 1,
|
||||||
|
DdMode::DdIpl => 2,
|
||||||
|
DdMode::Full => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BootMode {
|
||||||
|
Menu,
|
||||||
|
Rom,
|
||||||
|
DdIpl,
|
||||||
|
DirectRom,
|
||||||
|
DirectDdIpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BootMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Menu => "Bootloader -> Menu from SD card",
|
||||||
|
Self::Rom => "Bootloader -> ROM",
|
||||||
|
Self::DdIpl => "Bootloader -> 64DD IPL",
|
||||||
|
Self::DirectRom => "ROM (direct)",
|
||||||
|
Self::DirectDdIpl => "64DD IPL (direct)",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for BootMode {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Menu,
|
||||||
|
1 => Self::Rom,
|
||||||
|
2 => Self::DdIpl,
|
||||||
|
3 => Self::DirectRom,
|
||||||
|
4 => Self::DirectDdIpl,
|
||||||
|
_ => return Err(Error::new("Unknown boot mode code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BootMode> for u32 {
|
||||||
|
fn from(value: BootMode) -> Self {
|
||||||
|
match value {
|
||||||
|
BootMode::Menu => 0,
|
||||||
|
BootMode::Rom => 1,
|
||||||
|
BootMode::DdIpl => 2,
|
||||||
|
BootMode::DirectRom => 3,
|
||||||
|
BootMode::DirectDdIpl => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SaveType {
|
||||||
|
None,
|
||||||
|
Eeprom4k,
|
||||||
|
Eeprom16k,
|
||||||
|
Sram,
|
||||||
|
Flashram,
|
||||||
|
SramBanked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SaveType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::None => "None",
|
||||||
|
Self::Eeprom4k => "EEPROM 4k",
|
||||||
|
Self::Eeprom16k => "EEPROM 16k",
|
||||||
|
Self::Sram => "SRAM",
|
||||||
|
Self::SramBanked => "SRAM banked",
|
||||||
|
Self::Flashram => "FlashRAM",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for SaveType {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::None,
|
||||||
|
1 => Self::Eeprom4k,
|
||||||
|
2 => Self::Eeprom16k,
|
||||||
|
3 => Self::Sram,
|
||||||
|
4 => Self::Flashram,
|
||||||
|
5 => Self::SramBanked,
|
||||||
|
_ => return Err(Error::new("Unknown save type code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SaveType> for u32 {
|
||||||
|
fn from(value: SaveType) -> Self {
|
||||||
|
match value {
|
||||||
|
SaveType::None => 0,
|
||||||
|
SaveType::Eeprom4k => 1,
|
||||||
|
SaveType::Eeprom16k => 2,
|
||||||
|
SaveType::Sram => 3,
|
||||||
|
SaveType::Flashram => 4,
|
||||||
|
SaveType::SramBanked => 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CicSeed {
|
||||||
|
Seed(u8),
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CicSeed {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let CicSeed::Seed(seed) = self {
|
||||||
|
f.write_fmt(format_args!("0x{seed:02X}"))
|
||||||
|
} else {
|
||||||
|
f.write_str("Auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for CicSeed {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(if value <= 0xFF {
|
||||||
|
Self::Seed(value as u8)
|
||||||
|
} else if value == 0xFFFF {
|
||||||
|
Self::Auto
|
||||||
|
} else {
|
||||||
|
return Err(Error::new("Unknown CIC seed code"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CicSeed> for u32 {
|
||||||
|
fn from(value: CicSeed) -> Self {
|
||||||
|
match value {
|
||||||
|
CicSeed::Seed(seed) => seed.into(),
|
||||||
|
CicSeed::Auto => 0xFFFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TvType {
|
||||||
|
PAL,
|
||||||
|
NTSC,
|
||||||
|
MPAL,
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TvType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::PAL => "PAL",
|
||||||
|
Self::NTSC => "NTSC",
|
||||||
|
Self::MPAL => "MPAL",
|
||||||
|
Self::Auto => "Auto",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for TvType {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::PAL,
|
||||||
|
1 => Self::NTSC,
|
||||||
|
2 => Self::MPAL,
|
||||||
|
3 => Self::Auto,
|
||||||
|
_ => return Err(Error::new("Unknown TV type code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TvType> for u32 {
|
||||||
|
fn from(value: TvType) -> Self {
|
||||||
|
match value {
|
||||||
|
TvType::PAL => 0,
|
||||||
|
TvType::NTSC => 1,
|
||||||
|
TvType::MPAL => 2,
|
||||||
|
TvType::Auto => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DdDriveType {
|
||||||
|
Retail,
|
||||||
|
Development,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DdDriveType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
DdDriveType::Retail => "Retail",
|
||||||
|
DdDriveType::Development => "Development",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for DdDriveType {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Retail,
|
||||||
|
1 => Self::Development,
|
||||||
|
_ => return Err(Error::new("Unknown 64DD drive type code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DdDriveType> for u32 {
|
||||||
|
fn from(value: DdDriveType) -> Self {
|
||||||
|
match value {
|
||||||
|
DdDriveType::Retail => 0,
|
||||||
|
DdDriveType::Development => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DdDiskState {
|
||||||
|
Ejected,
|
||||||
|
Inserted,
|
||||||
|
Changed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DdDiskState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
DdDiskState::Ejected => "Ejected",
|
||||||
|
DdDiskState::Inserted => "Inserted",
|
||||||
|
DdDiskState::Changed => "Changed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for DdDiskState {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Ejected,
|
||||||
|
1 => Self::Inserted,
|
||||||
|
2 => Self::Changed,
|
||||||
|
_ => return Err(Error::new("Unknown 64DD disk state code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DdDiskState> for u32 {
|
||||||
|
fn from(value: DdDiskState) -> Self {
|
||||||
|
match value {
|
||||||
|
DdDiskState::Ejected => 0,
|
||||||
|
DdDiskState::Inserted => 1,
|
||||||
|
DdDiskState::Changed => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ButtonState {
|
||||||
|
NotPressed,
|
||||||
|
Pressed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ButtonState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
ButtonState::NotPressed => "Not pressed",
|
||||||
|
ButtonState::Pressed => "Pressed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for ButtonState {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::NotPressed,
|
||||||
|
_ => Self::Pressed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ButtonState> for u32 {
|
||||||
|
fn from(value: ButtonState) -> Self {
|
||||||
|
match value {
|
||||||
|
ButtonState::NotPressed => 0,
|
||||||
|
ButtonState::Pressed => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for ButtonState {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
match value {
|
||||||
|
false => Self::NotPressed,
|
||||||
|
true => Self::Pressed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ButtonState> for bool {
|
||||||
|
fn from(value: ButtonState) -> Self {
|
||||||
|
match value {
|
||||||
|
ButtonState::NotPressed => false,
|
||||||
|
ButtonState::Pressed => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ButtonMode {
|
||||||
|
None,
|
||||||
|
N64Irq,
|
||||||
|
UsbPacket,
|
||||||
|
DdDiskSwap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ButtonMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
ButtonMode::None => "User",
|
||||||
|
ButtonMode::N64Irq => "N64 IRQ trigger",
|
||||||
|
ButtonMode::UsbPacket => "Send USB packet",
|
||||||
|
ButtonMode::DdDiskSwap => "Swap 64DD disk",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for ButtonMode {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::None,
|
||||||
|
1 => Self::N64Irq,
|
||||||
|
2 => Self::UsbPacket,
|
||||||
|
3 => Self::DdDiskSwap,
|
||||||
|
_ => return Err(Error::new("Unknown button mode code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ButtonMode> for u32 {
|
||||||
|
fn from(value: ButtonMode) -> Self {
|
||||||
|
match value {
|
||||||
|
ButtonMode::None => 0,
|
||||||
|
ButtonMode::N64Irq => 1,
|
||||||
|
ButtonMode::UsbPacket => 2,
|
||||||
|
ButtonMode::DdDiskSwap => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum SettingId {
|
||||||
|
LedEnable,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Setting {
|
||||||
|
LedEnable(Switch),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SettingId> for u32 {
|
||||||
|
fn from(value: SettingId) -> Self {
|
||||||
|
match value {
|
||||||
|
SettingId::LedEnable => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(SettingId, u32)> for Setting {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: (SettingId, u32)) -> Result<Self, Self::Error> {
|
||||||
|
let (id, setting) = value;
|
||||||
|
Ok(match id {
|
||||||
|
SettingId::LedEnable => Self::LedEnable(setting.try_into()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Setting> for [u32; 2] {
|
||||||
|
fn from(value: Setting) -> Self {
|
||||||
|
match value {
|
||||||
|
Setting::LedEnable(val) => [SettingId::LedEnable.into(), val.into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DataPacket {
|
||||||
|
Button,
|
||||||
|
Debug(DebugPacket),
|
||||||
|
Disk(DiskPacket),
|
||||||
|
IsViewer(String),
|
||||||
|
UpdateStatus(UpdateStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Packet> for DataPacket {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Packet) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value.id {
|
||||||
|
b'B' => Self::Button,
|
||||||
|
b'U' => Self::Debug(value.data.try_into()?),
|
||||||
|
b'D' => Self::Disk(value.data.try_into()?),
|
||||||
|
b'I' => Self::IsViewer(EUC_JP.decode(&value.data).0.into()),
|
||||||
|
b'F' => {
|
||||||
|
if value.data.len() != 4 {
|
||||||
|
return Err(Error::new(
|
||||||
|
"Incorrect data length for update status data packet",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Self::UpdateStatus(
|
||||||
|
u32::from_be_bytes(value.data[0..4].try_into().unwrap()).try_into()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return Err(Error::new("Unknown data packet code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DebugPacket {
|
||||||
|
pub datatype: u8,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for DebugPacket {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() < 4 {
|
||||||
|
return Err(Error::new("Couldn't extract header from debug packet"));
|
||||||
|
}
|
||||||
|
let header = u32::from_be_bytes(value[0..4].try_into().unwrap());
|
||||||
|
let datatype = ((header >> 24) & 0xFF) as u8;
|
||||||
|
let length = (header & 0x00FFFFFF) as usize;
|
||||||
|
let data = value[4..].to_vec();
|
||||||
|
if data.len() != length {
|
||||||
|
return Err(Error::new("Debug packet length did not match"));
|
||||||
|
}
|
||||||
|
Ok(Self { datatype, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DiskPacketKind {
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiskPacket {
|
||||||
|
pub kind: DiskPacketKind,
|
||||||
|
pub info: DiskBlock,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for DiskPacket {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() < 12 {
|
||||||
|
return Err(Error::new("Couldn't extract block info from disk packet"));
|
||||||
|
}
|
||||||
|
let command = u32::from_be_bytes(value[0..4].try_into().unwrap());
|
||||||
|
let address = u32::from_be_bytes(value[4..8].try_into().unwrap());
|
||||||
|
let track_head_block = u32::from_be_bytes(value[8..12].try_into().unwrap());
|
||||||
|
let disk_block = DiskBlock {
|
||||||
|
address,
|
||||||
|
track: (track_head_block >> 2) & 0xFFF,
|
||||||
|
head: (track_head_block >> 1) & 0x01,
|
||||||
|
block: track_head_block & 0x01,
|
||||||
|
data: value[12..].to_vec(),
|
||||||
|
};
|
||||||
|
Ok(match command {
|
||||||
|
1 => DiskPacket {
|
||||||
|
kind: DiskPacketKind::Read,
|
||||||
|
info: disk_block,
|
||||||
|
},
|
||||||
|
2 => DiskPacket {
|
||||||
|
kind: DiskPacketKind::Write,
|
||||||
|
info: disk_block,
|
||||||
|
},
|
||||||
|
_ => return Err(Error::new("Unknown disk packet command code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiskBlock {
|
||||||
|
pub address: u32,
|
||||||
|
pub track: u32,
|
||||||
|
pub head: u32,
|
||||||
|
pub block: u32,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskBlock {
|
||||||
|
pub fn set_data(&mut self, data: &[u8]) {
|
||||||
|
self.data = data.to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FirmwareStatus {
|
||||||
|
Ok,
|
||||||
|
ErrToken,
|
||||||
|
ErrChecksum,
|
||||||
|
ErrSize,
|
||||||
|
ErrUnknownChunk,
|
||||||
|
ErrRead,
|
||||||
|
ErrAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FirmwareStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
FirmwareStatus::Ok => "OK",
|
||||||
|
FirmwareStatus::ErrToken => "Invalid firmware header",
|
||||||
|
FirmwareStatus::ErrChecksum => "Invalid chunk checksum",
|
||||||
|
FirmwareStatus::ErrSize => "Invalid chunk size",
|
||||||
|
FirmwareStatus::ErrUnknownChunk => "Unknown chunk in firmware",
|
||||||
|
FirmwareStatus::ErrRead => "Firmware read error",
|
||||||
|
FirmwareStatus::ErrAddress => "Invalid address or length provided",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for FirmwareStatus {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Ok,
|
||||||
|
1 => Self::ErrToken,
|
||||||
|
2 => Self::ErrChecksum,
|
||||||
|
3 => Self::ErrSize,
|
||||||
|
4 => Self::ErrUnknownChunk,
|
||||||
|
5 => Self::ErrRead,
|
||||||
|
6 => Self::ErrAddress,
|
||||||
|
_ => return Err(Error::new("Unknown firmware status code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UpdateStatus {
|
||||||
|
MCU,
|
||||||
|
FPGA,
|
||||||
|
Bootloader,
|
||||||
|
Done,
|
||||||
|
Err,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for UpdateStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
UpdateStatus::MCU => "Microcontroller",
|
||||||
|
UpdateStatus::FPGA => "FPGA",
|
||||||
|
UpdateStatus::Bootloader => "Bootloader",
|
||||||
|
UpdateStatus::Done => "Done",
|
||||||
|
UpdateStatus::Err => "Error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u32> for UpdateStatus {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
1 => Self::MCU,
|
||||||
|
2 => Self::FPGA,
|
||||||
|
3 => Self::Bootloader,
|
||||||
|
0x80 => Self::Done,
|
||||||
|
0xFF => Self::Err,
|
||||||
|
_ => return Err(Error::new("Unknown update status code")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FpgaDebugData {
|
||||||
|
pub last_pi_address: u32,
|
||||||
|
pub read_fifo_wait: bool,
|
||||||
|
pub read_fifo_failure: bool,
|
||||||
|
pub write_fifo_wait: bool,
|
||||||
|
pub write_fifo_failure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for FpgaDebugData {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 8 {
|
||||||
|
return Err(Error::new("Invalid data length for FPGA debug data"));
|
||||||
|
}
|
||||||
|
Ok(FpgaDebugData {
|
||||||
|
last_pi_address: u32::from_be_bytes(value[0..4].try_into().unwrap()),
|
||||||
|
read_fifo_wait: (value[7] & (1 << 0)) != 0,
|
||||||
|
read_fifo_failure: (value[7] & (1 << 1)) != 0,
|
||||||
|
write_fifo_wait: (value[7] & (1 << 2)) != 0,
|
||||||
|
write_fifo_failure: (value[7] & (1 << 3)) != 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FpgaDebugData {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!("PI address: 0x{:08X}", self.last_pi_address))?;
|
||||||
|
if self.read_fifo_wait {
|
||||||
|
f.write_str(" RW")?;
|
||||||
|
}
|
||||||
|
if self.read_fifo_failure {
|
||||||
|
f.write_str(" RF")?;
|
||||||
|
}
|
||||||
|
if self.write_fifo_wait {
|
||||||
|
f.write_str(" WW")?;
|
||||||
|
}
|
||||||
|
if self.write_fifo_failure {
|
||||||
|
f.write_str(" WF")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct McuStackUsage {
|
||||||
|
pub cic: u32,
|
||||||
|
pub rtc: u32,
|
||||||
|
pub led: u32,
|
||||||
|
pub gvr: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for McuStackUsage {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 16 {
|
||||||
|
return Err(Error::new("Invalid data length for MCU stack usage"));
|
||||||
|
}
|
||||||
|
Ok(McuStackUsage {
|
||||||
|
cic: u32::from_be_bytes(value[0..4].try_into().unwrap()),
|
||||||
|
rtc: u32::from_be_bytes(value[4..8].try_into().unwrap()),
|
||||||
|
led: u32::from_be_bytes(value[8..12].try_into().unwrap()),
|
||||||
|
gvr: u32::from_be_bytes(value[12..16].try_into().unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for McuStackUsage {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"CIC: {}, RTC: {}, LED: {}, GVR: {}",
|
||||||
|
self.cic, self.rtc, self.led, self.gvr
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! get_config {
|
||||||
|
($sc64:ident, $config:ident) => {{
|
||||||
|
if let Config::$config(value) = $sc64.command_config_get(ConfigId::$config)? {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::new("Unexpected config type"))
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! get_setting {
|
||||||
|
($sc64:ident, $setting:ident) => {{
|
||||||
|
// Note: remove 'allow(irrefutable_let_patterns)' below when more settings are added
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
if let Setting::$setting(value) = $sc64.command_setting_get(SettingId::$setting)? {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::new("Unexpected setting type"))
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use get_config;
|
||||||
|
pub(crate) use get_setting;
|
39
sw/deployer/src/sc64/utils.rs
Normal file
39
sw/deployer/src/sc64/utils.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use super::Error;
|
||||||
|
use chrono::{DateTime, Datelike, Local, NaiveDateTime, TimeZone, Timelike};
|
||||||
|
|
||||||
|
pub fn u8_from_bcd(value: u8) -> u8 {
|
||||||
|
(((value & 0xF0) >> 4) * 10) + (value & 0x0F)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bcd_from_u8(value: u8) -> u8 {
|
||||||
|
(((value / 10) & 0x0F) << 4) | ((value % 10) & 0x0F)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_to_datetime(data: &[u8; 8]) -> Result<DateTime<Local>, Error> {
|
||||||
|
let hour = u8_from_bcd(data[1]);
|
||||||
|
let minute = u8_from_bcd(data[2]);
|
||||||
|
let second = u8_from_bcd(data[3]);
|
||||||
|
let year = u8_from_bcd(data[5]) as u32 + 2000;
|
||||||
|
let month = u8_from_bcd(data[6]);
|
||||||
|
let day = u8_from_bcd(data[7]);
|
||||||
|
let native = &NaiveDateTime::parse_from_str(
|
||||||
|
&format!("{year:02}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"),
|
||||||
|
"%Y-%m-%dT%H:%M:%S",
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::new("Couldn't convert from bytes to DateTime<Local>"))?;
|
||||||
|
Ok(Local.from_local_datetime(native).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_from_datetime(datetime: DateTime<Local>) -> [u32; 2] {
|
||||||
|
let weekday = bcd_from_u8((datetime.weekday() as u8) + 1);
|
||||||
|
let hour = bcd_from_u8(datetime.hour() as u8);
|
||||||
|
let minute = bcd_from_u8(datetime.minute() as u8);
|
||||||
|
let second = bcd_from_u8(datetime.second() as u8);
|
||||||
|
let year = bcd_from_u8((datetime.year() - 2000) as u8);
|
||||||
|
let month = bcd_from_u8(datetime.month() as u8);
|
||||||
|
let day = bcd_from_u8(datetime.day() as u8);
|
||||||
|
[
|
||||||
|
u32::from_be_bytes([weekday, hour, minute, second]),
|
||||||
|
u32::from_be_bytes([0, year, month, day]),
|
||||||
|
]
|
||||||
|
}
|
11
sw/pc/.gitignore
vendored
11
sw/pc/.gitignore
vendored
@ -1,11 +0,0 @@
|
|||||||
/__pycache__
|
|
||||||
/build
|
|
||||||
/dist
|
|
||||||
*.bin
|
|
||||||
*.eep
|
|
||||||
*.fla
|
|
||||||
*.n64
|
|
||||||
*.spec
|
|
||||||
*.srm
|
|
||||||
*.v64
|
|
||||||
*.z64
|
|
1422
sw/pc/sc64.py
1422
sw/pc/sc64.py
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import queue
|
||||||
import serial
|
import serial
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from binascii import crc32
|
from binascii import crc32
|
||||||
from sc64 import SC64
|
from enum import IntEnum
|
||||||
|
from serial.tools import list_ports
|
||||||
from sys import exit
|
from sys import exit
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
@ -61,6 +63,7 @@ class SC64UpdateDataException(Exception):
|
|||||||
|
|
||||||
class SC64UpdateData:
|
class SC64UpdateData:
|
||||||
__UPDATE_TOKEN = b'SC64 Update v2.0'
|
__UPDATE_TOKEN = b'SC64 Update v2.0'
|
||||||
|
|
||||||
__CHUNK_ID_UPDATE_INFO = 1
|
__CHUNK_ID_UPDATE_INFO = 1
|
||||||
__CHUNK_ID_MCU_DATA = 2
|
__CHUNK_ID_MCU_DATA = 2
|
||||||
__CHUNK_ID_FPGA_DATA = 3
|
__CHUNK_ID_FPGA_DATA = 3
|
||||||
@ -73,6 +76,14 @@ class SC64UpdateData:
|
|||||||
__bootloader_data: Optional[bytes]
|
__bootloader_data: Optional[bytes]
|
||||||
__primer_data: Optional[bytes]
|
__primer_data: Optional[bytes]
|
||||||
|
|
||||||
|
def __int_to_bytes(self, value: int) -> bytes:
|
||||||
|
return value.to_bytes(4, byteorder='little')
|
||||||
|
|
||||||
|
def __align(self, value: int) -> int:
|
||||||
|
if (value % 16 != 0):
|
||||||
|
value += (16 - (value % 16))
|
||||||
|
return value
|
||||||
|
|
||||||
def __load_int(self, f: io.BufferedReader) -> int:
|
def __load_int(self, f: io.BufferedReader) -> int:
|
||||||
try:
|
try:
|
||||||
data = f.read(4)
|
data = f.read(4)
|
||||||
@ -156,6 +167,24 @@ class SC64UpdateData:
|
|||||||
def get_primer_data(self) -> Optional[bytes]:
|
def get_primer_data(self) -> Optional[bytes]:
|
||||||
return self.__primer_data
|
return self.__primer_data
|
||||||
|
|
||||||
|
def create_bootloader_only_firmware(self):
|
||||||
|
if (self.__bootloader_data == None):
|
||||||
|
raise SC64UpdateDataException('No bootloader data available for firmware creation')
|
||||||
|
|
||||||
|
chunk = b''
|
||||||
|
chunk += self.__int_to_bytes(self.__CHUNK_ID_BOOTLOADER_DATA)
|
||||||
|
chunk += self.__int_to_bytes(8 + self.__align(len(self.__bootloader_data)))
|
||||||
|
chunk += self.__int_to_bytes(crc32(self.__bootloader_data))
|
||||||
|
chunk += self.__int_to_bytes(len(self.__bootloader_data))
|
||||||
|
chunk += self.__bootloader_data
|
||||||
|
chunk += bytes([0] * (self.__align(len(chunk)) - len(chunk)))
|
||||||
|
|
||||||
|
data = b''
|
||||||
|
data += self.__UPDATE_TOKEN
|
||||||
|
data += chunk
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class STM32BootloaderException(Exception):
|
class STM32BootloaderException(Exception):
|
||||||
pass
|
pass
|
||||||
@ -408,6 +437,138 @@ class LCMXO2Primer:
|
|||||||
self.__cmd_execute(self.__CMD_RESTART)
|
self.__cmd_execute(self.__CMD_RESTART)
|
||||||
|
|
||||||
|
|
||||||
|
class SC64Exception(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SC64:
|
||||||
|
__serial: Optional[serial.Serial] = None
|
||||||
|
__packets = queue.Queue()
|
||||||
|
|
||||||
|
class __UpdateStatus(IntEnum):
|
||||||
|
MCU = 1
|
||||||
|
FPGA = 2
|
||||||
|
BOOTLOADER = 3
|
||||||
|
DONE = 0x80
|
||||||
|
ERROR = 0xFF
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
SC64_VID = 0x0403
|
||||||
|
SC64_PID = 0x6014
|
||||||
|
SC64_SID = "SC64"
|
||||||
|
for p in list_ports.comports():
|
||||||
|
if (p.vid == SC64_VID and p.pid == SC64_PID and p.serial_number.startswith(SC64_SID)):
|
||||||
|
try:
|
||||||
|
self.__serial = serial.Serial(p.device, timeout=10.0, write_timeout=10.0)
|
||||||
|
except serial.SerialException:
|
||||||
|
if (self.__serial):
|
||||||
|
self.__serial.close()
|
||||||
|
continue
|
||||||
|
return
|
||||||
|
raise SC64Exception('No SC64 USB device found')
|
||||||
|
|
||||||
|
def __reset(self) -> None:
|
||||||
|
WAIT_DURATION = 0.01
|
||||||
|
RETRY_COUNT = 100
|
||||||
|
|
||||||
|
self.__serial.dtr = 1
|
||||||
|
for n in range(0, RETRY_COUNT + 1):
|
||||||
|
self.__serial.reset_input_buffer()
|
||||||
|
self.__serial.reset_output_buffer()
|
||||||
|
time.sleep(WAIT_DURATION)
|
||||||
|
if (self.__serial.dsr == 1):
|
||||||
|
break
|
||||||
|
if n == RETRY_COUNT:
|
||||||
|
raise SC64Exception('Couldn\'t reset SC64 device (on)')
|
||||||
|
|
||||||
|
self.__serial.dtr = 0
|
||||||
|
for n in range(0, RETRY_COUNT + 1):
|
||||||
|
time.sleep(WAIT_DURATION)
|
||||||
|
if (self.__serial.dsr == 0):
|
||||||
|
break
|
||||||
|
if n == RETRY_COUNT:
|
||||||
|
raise SC64Exception('Couldn\'t reset SC64 device (on)')
|
||||||
|
|
||||||
|
def __process_incoming_data(self, wait_for_response: bool) -> Optional[tuple[bytes, bytes]]:
|
||||||
|
while (wait_for_response or self.__serial.in_waiting >= 4):
|
||||||
|
buffer = self.__serial.read(4)
|
||||||
|
token = buffer[0:3]
|
||||||
|
id = buffer[3:4]
|
||||||
|
if (token == b'CMP'):
|
||||||
|
length = int.from_bytes(self.__serial.read(4), byteorder='big')
|
||||||
|
data = self.__serial.read(length)
|
||||||
|
return (id, data)
|
||||||
|
elif (token == b'PKT'):
|
||||||
|
length = int.from_bytes(self.__serial.read(4), byteorder='big')
|
||||||
|
data = self.__serial.read(length)
|
||||||
|
self.__packets.put((id, data))
|
||||||
|
if (not wait_for_response):
|
||||||
|
break
|
||||||
|
elif (token == b'ERR'):
|
||||||
|
raise SC64Exception('Command response error')
|
||||||
|
else:
|
||||||
|
raise SC64Exception('Invalid token received')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __execute_command(self, cmd: bytes, args: list[int]=[0, 0], data: bytes=b'') -> bytes:
|
||||||
|
if (len(cmd) != 1):
|
||||||
|
raise SC64Exception('Length of command is different than 1 byte')
|
||||||
|
if (len(args) != 2):
|
||||||
|
raise SC64Exception('Number of arguments is different than 2')
|
||||||
|
try:
|
||||||
|
self.__serial.write(b'CMD' + cmd)
|
||||||
|
self.__serial.write(args[0].to_bytes(4, byteorder='big'))
|
||||||
|
self.__serial.write(args[1].to_bytes(4, byteorder='big'))
|
||||||
|
if (len(data) > 0):
|
||||||
|
self.__serial.write(data)
|
||||||
|
self.__serial.flush()
|
||||||
|
(id, response) = self.__process_incoming_data(True)
|
||||||
|
if (cmd != id):
|
||||||
|
raise SC64Exception('Command response ID didn\'t match')
|
||||||
|
return response
|
||||||
|
except serial.SerialException as e:
|
||||||
|
raise SC64Exception(f'Serial exception: {e}')
|
||||||
|
|
||||||
|
def __receive_data_packet(self) -> Optional[tuple[bytes, bytes]]:
|
||||||
|
if (self.__packets.empty()):
|
||||||
|
try:
|
||||||
|
if (self.__process_incoming_data(False) != None):
|
||||||
|
raise SC64Exception('Unexpected command response')
|
||||||
|
except serial.SerialException as e:
|
||||||
|
raise SC64Exception(f'Serial exception: {e}')
|
||||||
|
if (not self.__packets.empty()):
|
||||||
|
packet = self.__packets.get()
|
||||||
|
self.__packets.task_done()
|
||||||
|
return packet
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_firmware(self, data: bytes) -> None:
|
||||||
|
FIRMWARE_ADDRESS = 0x00100000
|
||||||
|
FIRMWARE_UPDATE_TIMEOUT = 90.0
|
||||||
|
|
||||||
|
self.__reset()
|
||||||
|
self.__execute_command(b'R')
|
||||||
|
self.__execute_command(b'M', [FIRMWARE_ADDRESS, len(data)], data)
|
||||||
|
self.__execute_command(b'F', [FIRMWARE_ADDRESS, len(data)])
|
||||||
|
|
||||||
|
timeout = time.time() + FIRMWARE_UPDATE_TIMEOUT
|
||||||
|
while True:
|
||||||
|
if (time.time() > timeout):
|
||||||
|
raise SC64Exception('Firmware update timeout')
|
||||||
|
packet = self.__receive_data_packet()
|
||||||
|
if (packet == None):
|
||||||
|
time.sleep(0.001)
|
||||||
|
continue
|
||||||
|
(id, packet_data) = packet
|
||||||
|
if (id != b'F'):
|
||||||
|
raise SC64Exception('Unexpected packet id received')
|
||||||
|
status = self.__UpdateStatus(int.from_bytes(packet_data[0:4], byteorder='big'))
|
||||||
|
if (status == self.__UpdateStatus.ERROR):
|
||||||
|
raise SC64Exception('Firmware update error')
|
||||||
|
if (status == self.__UpdateStatus.DONE):
|
||||||
|
time.sleep(2)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
class SC64BringUp:
|
class SC64BringUp:
|
||||||
__SERIAL_BAUD: int = 115200
|
__SERIAL_BAUD: int = 115200
|
||||||
__SERIAL_TIMEOUT: float = 6.0
|
__SERIAL_TIMEOUT: float = 6.0
|
||||||
@ -419,14 +580,17 @@ class SC64BringUp:
|
|||||||
def load_update_data(self, path: str) -> None:
|
def load_update_data(self, path: str) -> None:
|
||||||
self.__sc64_update_data = SC64UpdateData()
|
self.__sc64_update_data = SC64UpdateData()
|
||||||
self.__sc64_update_data.load(path, require_all=True)
|
self.__sc64_update_data.load(path, require_all=True)
|
||||||
|
self.__bootloader_only_firmware = self.__sc64_update_data.create_bootloader_only_firmware()
|
||||||
|
|
||||||
def get_update_info(self) -> str:
|
def get_update_info(self) -> str:
|
||||||
return self.__sc64_update_data.get_update_info()
|
return self.__sc64_update_data.get_update_info()
|
||||||
|
|
||||||
def start_bring_up(self, port: str, only_bootloader: bool=False) -> None:
|
def start_bring_up(self, port: str, bootloader_only: bool=False) -> None:
|
||||||
link = None
|
link = None
|
||||||
|
sc64 = SC64()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if (not only_bootloader):
|
if (not bootloader_only):
|
||||||
link = serial.Serial(
|
link = serial.Serial(
|
||||||
port,
|
port,
|
||||||
baudrate=self.__SERIAL_BAUD,
|
baudrate=self.__SERIAL_BAUD,
|
||||||
@ -453,11 +617,10 @@ class SC64BringUp:
|
|||||||
time.sleep(self.__INTERVAL_TIME)
|
time.sleep(self.__INTERVAL_TIME)
|
||||||
link.read_all()
|
link.read_all()
|
||||||
|
|
||||||
bootloader_description = 'Bootloader -> SC64 FLASH'
|
bootloader_description = 'Bootloader -> SC64 FLASH (no progress reporting)'
|
||||||
bootloader_data = self.__sc64_update_data.get_bootloader_data()
|
bootloader_length = len(self.__bootloader_only_firmware)
|
||||||
bootloader_length = len(bootloader_data)
|
|
||||||
self.__progress(bootloader_length, 0, bootloader_description)
|
self.__progress(bootloader_length, 0, bootloader_description)
|
||||||
SC64().upload_bootloader(bootloader_data)
|
sc64.update_firmware(self.__bootloader_only_firmware)
|
||||||
self.__progress(bootloader_length, bootloader_length, bootloader_description)
|
self.__progress(bootloader_length, bootloader_length, bootloader_description)
|
||||||
finally:
|
finally:
|
||||||
if (link and link.is_open):
|
if (link and link.is_open):
|
||||||
@ -467,14 +630,14 @@ class SC64BringUp:
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
nargs = len(sys.argv)
|
nargs = len(sys.argv)
|
||||||
if (nargs < 3 or nargs > 4):
|
if (nargs < 3 or nargs > 4):
|
||||||
Utils.die(f'Usage: {sys.argv[0]} serial_port update_file [--only-bootloader]')
|
Utils.die(f'Usage: {sys.argv[0]} serial_port update_file [--bootloader-only]')
|
||||||
|
|
||||||
port = sys.argv[1]
|
port = sys.argv[1]
|
||||||
update_data_path = sys.argv[2]
|
update_data_path = sys.argv[2]
|
||||||
only_bootloader = False
|
bootloader_only = False
|
||||||
if (nargs == 4):
|
if (nargs == 4):
|
||||||
if (sys.argv[3] == '--only-bootloader'):
|
if (sys.argv[3] == '--bootloader-only'):
|
||||||
only_bootloader = True
|
bootloader_only = True
|
||||||
else:
|
else:
|
||||||
Utils.die(f'Unknown argument: {sys.argv[3]}')
|
Utils.die(f'Unknown argument: {sys.argv[3]}')
|
||||||
|
|
||||||
@ -491,12 +654,12 @@ if __name__ == '__main__':
|
|||||||
sc64_bring_up.load_update_data(update_data_path)
|
sc64_bring_up.load_update_data(update_data_path)
|
||||||
except SC64UpdateDataException as e:
|
except SC64UpdateDataException as e:
|
||||||
Utils.die(f'Provided \'{update_data_path}\' file is invalid: {e}')
|
Utils.die(f'Provided \'{update_data_path}\' file is invalid: {e}')
|
||||||
Utils.log_no_end('Update info: ')
|
Utils.log('Update info: ')
|
||||||
Utils.log(sc64_bring_up.get_update_info())
|
Utils.log(sc64_bring_up.get_update_info())
|
||||||
Utils.log()
|
Utils.log()
|
||||||
|
|
||||||
if (only_bootloader):
|
if bootloader_only:
|
||||||
Utils.log('Running in "only bootloader" mode')
|
Utils.log('Running in "bootloader only" mode')
|
||||||
Utils.log()
|
Utils.log()
|
||||||
|
|
||||||
Utils.warning('[ CAUTION ]')
|
Utils.warning('[ CAUTION ]')
|
||||||
@ -520,8 +683,8 @@ if __name__ == '__main__':
|
|||||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||||
try:
|
try:
|
||||||
signal.signal(signal.SIGINT, lambda *kwargs: utils.exit_warning())
|
signal.signal(signal.SIGINT, lambda *kwargs: utils.exit_warning())
|
||||||
sc64_bring_up.start_bring_up(port, only_bootloader)
|
sc64_bring_up.start_bring_up(port, bootloader_only)
|
||||||
except (serial.SerialException, STM32BootloaderException, LCMXO2PrimerException) as e:
|
except (serial.SerialException, STM32BootloaderException, LCMXO2PrimerException, SC64Exception) as e:
|
||||||
if (utils.get_progress_active):
|
if (utils.get_progress_active):
|
||||||
Utils.log()
|
Utils.log()
|
||||||
Utils.die(f'Error while running bring-up: {e}')
|
Utils.die(f'Error while running bring-up: {e}')
|
@ -1,2 +1 @@
|
|||||||
Pillow==9.2.0
|
|
||||||
pyserial==3.5
|
pyserial==3.5
|
23
sw/update/update.py → sw/tools/update.py
Executable file → Normal file
23
sw/update/update.py → sw/tools/update.py
Executable file → Normal file
@ -145,13 +145,13 @@ class JedecFile:
|
|||||||
|
|
||||||
|
|
||||||
class SC64UpdateData:
|
class SC64UpdateData:
|
||||||
__UPDATE_TOKEN = b'SC64 Update v2.0'
|
__UPDATE_TOKEN = b'SC64 Update v2.0'
|
||||||
|
|
||||||
__CHUNK_ID_UPDATE_INFO = 1
|
__CHUNK_ID_UPDATE_INFO = 1
|
||||||
__CHUNK_ID_MCU_DATA = 2
|
__CHUNK_ID_MCU_DATA = 2
|
||||||
__CHUNK_ID_FPGA_DATA = 3
|
__CHUNK_ID_FPGA_DATA = 3
|
||||||
__CHUNK_ID_BOOTLOADER_DATA = 4
|
__CHUNK_ID_BOOTLOADER_DATA = 4
|
||||||
__CHUNK_ID_PRIMER_DATA = 5
|
__CHUNK_ID_PRIMER_DATA = 5
|
||||||
|
|
||||||
__data = b''
|
__data = b''
|
||||||
|
|
||||||
@ -219,13 +219,12 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
hostname = platform.node()
|
hostname = platform.node()
|
||||||
creation_datetime = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
|
creation_datetime = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
info = [
|
update_info = '\n'.join([
|
||||||
f'build system: [{hostname}]',
|
f'builder: {hostname}',
|
||||||
f'creation datetime: [{creation_datetime}]',
|
f'created: {creation_datetime}',
|
||||||
]
|
])
|
||||||
if (args.git):
|
if (args.git):
|
||||||
info.append(args.git)
|
update_info += args.git
|
||||||
update_info = ' '.join(info)
|
|
||||||
print(update_info)
|
print(update_info)
|
||||||
update.add_update_info(update_info.encode())
|
update.add_update_info(update_info.encode())
|
||||||
|
|
1
sw/update/.gitignore
vendored
1
sw/update/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*.bin
|
|
Loading…
Reference in New Issue
Block a user