354 lines
9.4 KiB
C++
354 lines
9.4 KiB
C++
/**
|
|
* Copyright (C) 2022 by Mahyar Koshkouei <mk@deltabeard.com>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
* PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#if ENABLE_SDCARD
|
|
|
|
#include <Arduino.h>
|
|
#include "SdFat.h"
|
|
#include "hardware/flash.h"
|
|
|
|
#include "common.h"
|
|
#include "input.h"
|
|
#include "gb.h"
|
|
#include "card_loader.h"
|
|
|
|
#define RAM_SAVENAME_LENGTH 16
|
|
|
|
SdFs sd;
|
|
|
|
gpio_function_t UseSDPinFunctionScope::sd_sck_pin_func = GPIO_FUNC_NULL;
|
|
gpio_function_t UseSDPinFunctionScope::sd_mosi_pin_func = GPIO_FUNC_NULL;
|
|
|
|
bool init_sdcard_hardware() {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
SD_SPI.setMISO(SD_MISO_PIN);
|
|
SD_SPI.setMOSI(SD_MOSI_PIN);
|
|
SD_SPI.setSCK(SD_SCK_PIN);
|
|
bool success = sd.begin(SdSpiConfig(SD_CS_PIN, SHARED_SPI, SD_SCK_MHZ(50), &SD_SPI));
|
|
if (success) {
|
|
UseSDPinFunctionScope::init();
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void init_sdcard() {
|
|
Serial.println("Initialize SD-Card ...");
|
|
|
|
if (!init_sdcard_hardware()) {
|
|
tft.setCursor(0, ERROR_TEXT_OFFSET, FONT_ID);
|
|
tft.setTextColor(TFT_RED);
|
|
sd.printSdError(&tft);
|
|
sd.printSdError(&Serial);
|
|
reset(5000);
|
|
}
|
|
if (sd.vol()->fatType() == 0) { // vol() and fatType() do not access the sd-card
|
|
error("Can't find a valid FAT16/FAT32/exFAT partition");
|
|
}
|
|
|
|
Serial.printf("SD-Card initialized: FAT-Type=%d\n", sd.vol()->fatType());
|
|
}
|
|
|
|
/**
|
|
* Load a save file from the SD card
|
|
*/
|
|
void read_cart_ram_file(struct gb_s* gb) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
char filename[RAM_SAVENAME_LENGTH];
|
|
uint_fast32_t save_size;
|
|
FsFile file;
|
|
|
|
gb_get_rom_name(gb, filename);
|
|
save_size = gb_get_save_size(gb);
|
|
if (save_size > 0) {
|
|
if (!file.open(filename, O_RDONLY)) {
|
|
Serial.printf("E f_open(%s) error\n", filename);
|
|
} else {
|
|
file.read(ram, file.size());
|
|
}
|
|
|
|
if (!file.close()) {
|
|
Serial.printf("E f_close error\n");
|
|
}
|
|
}
|
|
|
|
Serial.printf("I read_cart_ram_file(%s) COMPLETE (%lu bytes)\n", filename, save_size);
|
|
}
|
|
|
|
/**
|
|
* Write a save file to the SD card
|
|
*/
|
|
void write_cart_ram_file(struct gb_s* gb) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
char filename[RAM_SAVENAME_LENGTH];
|
|
uint_fast32_t save_size;
|
|
FsFile file;
|
|
|
|
gb_get_rom_name(gb, filename);
|
|
save_size = gb_get_save_size(gb);
|
|
if (save_size > 0) {
|
|
if (!file.open(filename, O_WRONLY | O_CREAT)) {
|
|
Serial.printf("E f_open(%s) error\n", filename);
|
|
return;
|
|
}
|
|
|
|
file.write(ram, save_size);
|
|
if (!file.close()) {
|
|
Serial.printf("E f_close error\n");
|
|
}
|
|
}
|
|
|
|
Serial.printf("I write_cart_ram_file(%s) COMPLETE (%lu bytes)\n", filename, save_size);
|
|
}
|
|
|
|
bool write_rom_sector_to_flash(FsFile& file, uint8_t* buffer, uint32_t offset) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
int nread = file.read(buffer, FLASH_SECTOR_SIZE);
|
|
if (nread < 0) {
|
|
scope.close();
|
|
error("Failed to read file!");
|
|
}
|
|
|
|
if (nread == 0) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t flash_offset = ((uint32_t) &rom[offset]) - XIP_BASE;
|
|
|
|
uint32_t ints = save_and_disable_interrupts();
|
|
flash_range_erase(flash_offset, FLASH_SECTOR_SIZE);
|
|
flash_range_program(flash_offset, buffer, FLASH_SECTOR_SIZE);
|
|
restore_interrupts(ints);
|
|
|
|
/* Read back target region and check programming */
|
|
if (memcmp(&rom[offset], buffer, FLASH_SECTOR_SIZE) != 0) {
|
|
scope.close();
|
|
error("Programming failed - Flash mismatch");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void open_rom_file(FsFile& file, char* filename) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
if (!file.open(filename, O_RDONLY)) {
|
|
scope.close();
|
|
error("Failed to open ROM: " + String(filename));
|
|
}
|
|
}
|
|
|
|
static void close_rom_file(FsFile& file) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
if (!file.close()) {
|
|
Serial.printf("E f_close error\n");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a .gb rom file in flash from the SD card
|
|
*/
|
|
void load_cart_rom_file(char* filename) {
|
|
uint8_t buffer[FLASH_SECTOR_SIZE];
|
|
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(0, 0, FONT_ID);
|
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
|
tft.println("Loading ROM: ");
|
|
|
|
FsFile file;
|
|
open_rom_file(file, filename);
|
|
|
|
Serial.printf("I Program target region...\n");
|
|
|
|
uint32_t offset = 0;
|
|
while (write_rom_sector_to_flash(file, buffer, offset)) {
|
|
tft.print("#");
|
|
|
|
/* Next sector */
|
|
offset += FLASH_SECTOR_SIZE;
|
|
}
|
|
|
|
close_rom_file(file);
|
|
|
|
Serial.printf("I load_cart_rom_file(%s) COMPLETE\n", filename);
|
|
}
|
|
|
|
static uint16_t read_file_page_from_card(char filename[FILES_PER_PAGE][MAX_PATH_LENGTH], uint16_t num_page) {
|
|
auto scope = UseSDPinFunctionScope();
|
|
|
|
FsFile dir;
|
|
FsFile file;
|
|
|
|
/* clear the filenames array */
|
|
for (uint8_t ifile = 0; ifile < FILES_PER_PAGE; ifile++) {
|
|
strcpy(filename[ifile], "");
|
|
}
|
|
|
|
if (!dir.open("/")) {
|
|
scope.close();
|
|
error("Failed to open root dir");
|
|
}
|
|
|
|
/* search *.gb files */
|
|
uint16_t num_files = 0;
|
|
uint16_t num_file_offset = num_page * FILES_PER_PAGE;
|
|
char currentFilename[MAX_PATH_LENGTH];
|
|
while (file.openNext(&dir, O_RDONLY)) {
|
|
file.getName(currentFilename, sizeof(currentFilename));
|
|
|
|
auto currentFilenameStr = String(currentFilename);
|
|
currentFilenameStr.toLowerCase();
|
|
if (!currentFilenameStr.endsWith(".gb") && !currentFilenameStr.endsWith(".gbc")) {
|
|
continue;
|
|
}
|
|
|
|
if (num_files < num_file_offset) {
|
|
// skip the first N pages
|
|
} else {
|
|
// store the filenames of this page
|
|
strcpy(filename[num_files], currentFilename);
|
|
}
|
|
|
|
num_files++;
|
|
file.close();
|
|
|
|
if (num_files >= num_file_offset + FILES_PER_PAGE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
dir.close();
|
|
|
|
return num_files;
|
|
}
|
|
|
|
|
|
void print_file_entry(char* s, uint8_t index, uint8_t num_files, bool selected = false) {
|
|
if (num_files == 0) {
|
|
const char* no_files = "<No files on card>";
|
|
tft.drawString(no_files, ERROR_TEXT_OFFSET, index * FONT_HEIGHT, FONT_ID);
|
|
return;
|
|
}
|
|
|
|
tft.setTextColor(TFT_WHITE, selected ? TFT_RED : TFT_BLACK);
|
|
tft.drawString(s, 0, index * FONT_HEIGHT, FONT_ID);
|
|
}
|
|
|
|
/**
|
|
* Function used by the rom file selector to display one page of .gb rom files
|
|
*/
|
|
static uint16_t rom_file_selector_display_page(char filename[FILES_PER_PAGE][MAX_PATH_LENGTH], uint16_t num_page) {
|
|
uint16_t num_files = read_file_page_from_card(filename, num_page);
|
|
|
|
/* display *.gb rom files on screen */
|
|
tft.fillScreen(TFT_BLACK);
|
|
for (uint8_t ifile = 0; ifile < num_files; ifile++) {
|
|
print_file_entry(filename[ifile], ifile, num_files);
|
|
}
|
|
|
|
return num_files;
|
|
}
|
|
|
|
/**
|
|
* The ROM selector displays pages of up to FILES_PER_PAGE rom files
|
|
* allowing the user to select which rom file to start
|
|
* Copy your *.gb rom files to the root directory of the SD card
|
|
*/
|
|
void rom_file_selector() {
|
|
uint16_t num_page = 0;
|
|
char filename[FILES_PER_PAGE][MAX_PATH_LENGTH];
|
|
uint16_t num_files;
|
|
|
|
/* display the first page with up to FILES_PER_PAGE rom files */
|
|
num_files = rom_file_selector_display_page(filename, num_page);
|
|
|
|
/* select the first rom */
|
|
uint8_t selected = 0;
|
|
print_file_entry(filename[selected], selected, num_files, true);
|
|
|
|
/* get user's input */
|
|
bool up, down, left, right, a, b, select, start;
|
|
while (true) {
|
|
up = readJoypad(PIN_UP);
|
|
down = readJoypad(PIN_DOWN);
|
|
left = readJoypad(PIN_LEFT);
|
|
right = readJoypad(PIN_RIGHT);
|
|
a = readJoypad(PIN_A);
|
|
b = readJoypad(PIN_B);
|
|
select = readJoypad(PIN_SELECT);
|
|
start = readJoypad(PIN_START);
|
|
if (!start) {
|
|
/* re-start the last game (no need to reprogram flash) */
|
|
break;
|
|
}
|
|
if (!a | !b) {
|
|
/* copy the rom from the SD card to flash and start the game */
|
|
load_cart_rom_file(filename[selected]);
|
|
break;
|
|
}
|
|
if (!down) {
|
|
/* select the next rom */
|
|
print_file_entry(filename[selected], selected, num_files);
|
|
selected++;
|
|
if (selected >= num_files)
|
|
selected = 0;
|
|
print_file_entry(filename[selected], selected, num_files, true);
|
|
sleep_ms(150);
|
|
}
|
|
if (!up) {
|
|
/* select the previous rom */
|
|
print_file_entry(filename[selected], selected, num_files);
|
|
if (selected == 0) {
|
|
selected = num_files - 1;
|
|
} else {
|
|
selected--;
|
|
}
|
|
print_file_entry(filename[selected], selected, num_files, true);
|
|
sleep_ms(150);
|
|
}
|
|
if (!right) {
|
|
/* select the next page */
|
|
num_page++;
|
|
num_files = rom_file_selector_display_page(filename, num_page);
|
|
if (num_files == 0) {
|
|
/* no files in this page, go to the previous page */
|
|
num_page--;
|
|
num_files = rom_file_selector_display_page(filename, num_page);
|
|
}
|
|
/* select the first file */
|
|
selected = 0;
|
|
print_file_entry(filename[selected], selected, num_files, true);
|
|
sleep_ms(150);
|
|
}
|
|
if ((!left) && num_page > 0) {
|
|
/* select the previous page */
|
|
num_page--;
|
|
num_files = rom_file_selector_display_page(filename, num_page);
|
|
/* select the first file */
|
|
selected = 0;
|
|
print_file_entry(filename[selected], selected, num_files, true);
|
|
sleep_ms(150);
|
|
}
|
|
tight_loop_contents();
|
|
}
|
|
}
|
|
|
|
#endif
|