mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-01-22 11:43:39 +01:00
Bump to 0.4.5, add flash executable loading, fix bugs
This commit is contained in:
parent
aac5f4abb1
commit
f4a8d16b20
@ -6,7 +6,7 @@ set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
|
||||
project(
|
||||
cart-tool
|
||||
LANGUAGES C CXX ASM
|
||||
VERSION 0.4.4
|
||||
VERSION 0.4.5
|
||||
DESCRIPTION "Konami System 573 security cartridge tool"
|
||||
)
|
||||
|
||||
|
@ -7,8 +7,8 @@
|
||||
extern "C" const uint8_t _resourceArchive[];
|
||||
extern "C" const size_t _resourceArchiveLength;
|
||||
|
||||
static char _ptrArg[]{ "resource.ptr=xxxxxxxx" };
|
||||
static char _lengthArg[]{ "resource.length=xxxxxxxx" };
|
||||
static char _ptrArg[]{ "resource.ptr=xxxxxxxx\0" };
|
||||
static char _lengthArg[]{ "resource.length=xxxxxxxx\0" };
|
||||
|
||||
struct [[gnu::packed]] ZIPFileHeader {
|
||||
public:
|
||||
|
@ -87,8 +87,8 @@ bool ExecutableLauncherArgs::parseArgument(const char *arg) {
|
||||
loadAddress = reinterpret_cast<void *>(strtol(&arg[5], nullptr, 16));
|
||||
return true;
|
||||
|
||||
case "drive"_h:
|
||||
drive = int(strtol(&arg[6], nullptr, 0));
|
||||
case "device"_h:
|
||||
device = int(strtol(&arg[6], nullptr, 0));
|
||||
return true;
|
||||
|
||||
case "frag"_h:
|
||||
|
@ -52,7 +52,7 @@ public:
|
||||
void *entryPoint, *initialGP, *stackTop;
|
||||
|
||||
void *loadAddress;
|
||||
int drive;
|
||||
int device; // 0-63 = flash, -1 or -2 = IDE
|
||||
|
||||
size_t numArgs, numFragments;
|
||||
const char *executableArgs[util::MAX_EXECUTABLE_ARGS];
|
||||
@ -60,7 +60,7 @@ public:
|
||||
|
||||
inline ExecutableLauncherArgs(void)
|
||||
: entryPoint(nullptr), initialGP(nullptr), stackTop(nullptr),
|
||||
loadAddress(nullptr), drive(0), numArgs(0), numFragments(0) {}
|
||||
loadAddress(nullptr), device(0), numArgs(0), numFragments(0) {}
|
||||
|
||||
bool parseArgument(const char *arg);
|
||||
};
|
||||
|
@ -104,6 +104,9 @@ void FATDirectory::close(void) {
|
||||
/* FAT filesystem provider */
|
||||
|
||||
bool FATProvider::init(int drive) {
|
||||
if (type)
|
||||
return false;
|
||||
|
||||
_drive[0] = drive + '0';
|
||||
|
||||
auto error = f_mount(&_fs, _drive, 1);
|
||||
@ -123,6 +126,9 @@ bool FATProvider::init(int drive) {
|
||||
}
|
||||
|
||||
void FATProvider::close(void) {
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
auto error = f_unmount(_drive);
|
||||
|
||||
if (error) {
|
||||
|
@ -20,11 +20,12 @@ enum FileSystemType {
|
||||
FAT12 = 1,
|
||||
FAT16 = 2,
|
||||
FAT32 = 3,
|
||||
ISO9660 = 4,
|
||||
ZIP_MEMORY = 5,
|
||||
ZIP_FILE = 6,
|
||||
HOST = 7,
|
||||
VFS = 8
|
||||
EXFAT = 4,
|
||||
ISO9660 = 5,
|
||||
ZIP_MEMORY = 6,
|
||||
ZIP_FILE = 7,
|
||||
HOST = 8,
|
||||
VFS = 9
|
||||
};
|
||||
|
||||
// These are functionally equivalent to the FA_* flags used by FatFs.
|
||||
|
@ -180,24 +180,33 @@ VFSMountPoint *VFSProvider::_getMounted(const char *path) {
|
||||
bool VFSProvider::mount(const char *prefix, Provider *provider, bool force) {
|
||||
auto hash = util::hash(prefix, VFS_PREFIX_SEPARATOR);
|
||||
|
||||
VFSMountPoint *freeMP = nullptr;
|
||||
|
||||
for (auto &mp : _mountPoints) {
|
||||
if (force) {
|
||||
if (mp.prefix && (mp.prefix != hash))
|
||||
continue;
|
||||
} else {
|
||||
if (mp.prefix)
|
||||
continue;
|
||||
if (!mp.prefix) {
|
||||
freeMP = ∓
|
||||
} else if (mp.prefix == hash) {
|
||||
if (force) {
|
||||
freeMP = ∓
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_FS("%s was already mapped", prefix);
|
||||
return false;
|
||||
}
|
||||
|
||||
mp.prefix = hash;
|
||||
mp.pathOffset = __builtin_strlen(prefix);
|
||||
mp.provider = provider;
|
||||
|
||||
LOG_FS("mapped %s", prefix);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!freeMP) {
|
||||
LOG_FS("no mount points left for %s", prefix);
|
||||
return false;
|
||||
}
|
||||
|
||||
freeMP->prefix = hash;
|
||||
freeMP->pathOffset = __builtin_strlen(prefix);
|
||||
freeMP->provider = provider;
|
||||
|
||||
LOG_FS("mapped %s", prefix);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VFSProvider::unmount(const char *prefix) {
|
||||
@ -215,6 +224,7 @@ bool VFSProvider::unmount(const char *prefix) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_FS("%s was not mapped", prefix);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -97,6 +97,9 @@ static constexpr uint32_t _ZIP_FLAGS = 0
|
||||
| MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY;
|
||||
|
||||
bool ZIPProvider::init(File *file) {
|
||||
if (type)
|
||||
return false;
|
||||
|
||||
mz_zip_zero_struct(&_zip);
|
||||
_file = file;
|
||||
|
||||
@ -128,6 +131,9 @@ bool ZIPProvider::init(File *file) {
|
||||
}
|
||||
|
||||
bool ZIPProvider::init(const void *zipData, size_t length) {
|
||||
if (type)
|
||||
return false;
|
||||
|
||||
mz_zip_zero_struct(&_zip);
|
||||
_file = nullptr;
|
||||
|
||||
@ -146,6 +152,9 @@ bool ZIPProvider::init(const void *zipData, size_t length) {
|
||||
}
|
||||
|
||||
void ZIPProvider::close(void) {
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
mz_zip_reader_end(&_zip);
|
||||
|
||||
#if 0
|
||||
|
@ -607,8 +607,9 @@ DeviceError Device::enumerate(void) {
|
||||
// actually present. A strict timeout is used in the commands below in order
|
||||
// to prevent blocking for too long.
|
||||
IdentifyBlock block;
|
||||
auto signature = _getCylinder();
|
||||
|
||||
if (_getCylinder() == _ATAPI_SIGNATURE) {
|
||||
if (signature == _ATAPI_SIGNATURE) {
|
||||
flags |= DEVICE_ATAPI;
|
||||
|
||||
_write(CS0_COMMAND, ATA_IDENTIFY_PACKET);
|
||||
@ -660,6 +661,13 @@ DeviceError Device::enumerate(void) {
|
||||
// Find out the fastest PIO transfer mode supported and enable it.
|
||||
int mode = block.getHighestPIOMode();
|
||||
|
||||
_select(0);
|
||||
|
||||
error = _waitForIdle();
|
||||
|
||||
if (error)
|
||||
return error;
|
||||
|
||||
_write(CS0_FEATURES, FEATURE_TRANSFER_MODE);
|
||||
_write(CS0_COUNT, TRANSFER_MODE_PIO | mode);
|
||||
_write(CS0_COMMAND, ATA_SET_FEATURES);
|
||||
|
@ -36,12 +36,12 @@ void init(void) {
|
||||
| ( 4 << 24) // DMA read/write delay
|
||||
| BIU_CTRL_DMA_DELAY;
|
||||
|
||||
#if 0
|
||||
// Revision D of the main board has footprints for either eight 8-bit RAM
|
||||
// chips wired as two 32-bit banks, or two 16-bit chips wired as a single
|
||||
// bank. Normally the kernel takes care of setting up the memory controller
|
||||
// appropriately, but this makes sure the configuration is correct if e.g.
|
||||
// the tool is booted through OpenBIOS instead.
|
||||
// bank.
|
||||
DRAM_CTRL = isDualBankRAM() ? 0x0c80 : 0x4788;
|
||||
#endif
|
||||
|
||||
_bankSwitchReg = 0;
|
||||
_cartOutputReg = 0;
|
||||
|
@ -29,9 +29,21 @@ void Region::read(void *data, uint32_t offset, size_t length) const {
|
||||
auto source = reinterpret_cast<const uint32_t *>(ptr + offset);
|
||||
auto dest = reinterpret_cast<uint32_t *>(data);
|
||||
|
||||
// TODO: use memcpy() instead once an optimized implementation is added
|
||||
util::assertAligned<uint32_t>(source);
|
||||
util::assertAligned<uint32_t>(dest);
|
||||
|
||||
for (; length >= 32; length -= 32, dest += 8, source += 8) {
|
||||
dest[0] = source[0];
|
||||
dest[1] = source[1];
|
||||
dest[2] = source[2];
|
||||
dest[3] = source[3];
|
||||
dest[4] = source[4];
|
||||
dest[5] = source[5];
|
||||
dest[6] = source[6];
|
||||
dest[7] = source[7];
|
||||
}
|
||||
|
||||
for (; length; length -= 4)
|
||||
*(dest++) = *(source++);
|
||||
}
|
||||
@ -64,8 +76,19 @@ void RTCRegion::read(void *data, uint32_t offset, size_t length) const {
|
||||
|
||||
// The RTC is an 8-bit device connected to a 16-bit bus, i.e. each byte must
|
||||
// be read as a 16-bit value and then the upper 8 bits must be discarded.
|
||||
for (; length >= 8; length -= 8, dest += 8, source += 8) {
|
||||
dest[0] = uint8_t(source[0]);
|
||||
dest[1] = uint8_t(source[1]);
|
||||
dest[2] = uint8_t(source[2]);
|
||||
dest[3] = uint8_t(source[3]);
|
||||
dest[4] = uint8_t(source[4]);
|
||||
dest[5] = uint8_t(source[5]);
|
||||
dest[6] = uint8_t(source[6]);
|
||||
dest[7] = uint8_t(source[7]);
|
||||
}
|
||||
|
||||
for (; length; length--)
|
||||
*(dest++) = *(source++) & 0xff;
|
||||
*(dest++) = uint8_t(*(source++));
|
||||
}
|
||||
|
||||
uint32_t RTCRegion::zipCRC32(
|
||||
@ -120,18 +143,11 @@ uint16_t *FlashRegion::getRawPtr(uint32_t offset, bool alignToChip) const {
|
||||
void FlashRegion::read(void *data, uint32_t offset, size_t length) const {
|
||||
// FIXME: this implementation will not handle unaligned reads and reads that
|
||||
// cross bank boundaries properly
|
||||
int bankOffset = offset / FLASH_BANK_LENGTH;
|
||||
int ptrOffset = offset % FLASH_BANK_LENGTH;
|
||||
auto bankOffset = offset / FLASH_BANK_LENGTH;
|
||||
auto ptrOffset = offset % FLASH_BANK_LENGTH;
|
||||
|
||||
auto source = reinterpret_cast<const uint32_t *>(ptr + ptrOffset);
|
||||
auto dest = reinterpret_cast<uint32_t *>(data);
|
||||
|
||||
util::assertAligned<uint32_t>(source);
|
||||
util::assertAligned<uint32_t>(dest);
|
||||
io::setFlashBank(bank + bankOffset);
|
||||
|
||||
for (; length; length -= 4)
|
||||
*(dest++) = *(source++);
|
||||
Region::read(data, ptrOffset, length);
|
||||
}
|
||||
|
||||
uint32_t FlashRegion::zipCRC32(
|
||||
@ -139,8 +155,8 @@ uint32_t FlashRegion::zipCRC32(
|
||||
) const {
|
||||
// FIXME: this implementation will not handle unaligned reads and reads that
|
||||
// cross bank boundaries properly
|
||||
int bankOffset = offset / FLASH_BANK_LENGTH;
|
||||
int ptrOffset = offset % FLASH_BANK_LENGTH;
|
||||
auto bankOffset = offset / FLASH_BANK_LENGTH;
|
||||
auto ptrOffset = offset % FLASH_BANK_LENGTH;
|
||||
|
||||
auto source = reinterpret_cast<const uint32_t *>(ptr + ptrOffset);
|
||||
auto table = reinterpret_cast<const uint32_t *>(CACHE_BASE);
|
||||
@ -176,7 +192,7 @@ enum FlashIdentifier : uint16_t {
|
||||
_ID_28F640J5 = 0x89 | (0x15 << 8)
|
||||
};
|
||||
|
||||
bool FlashRegion::hasBootExecutable(void) const {
|
||||
const util::ExecutableHeader *FlashRegion::getBootExecutableHeader(void) const {
|
||||
// FIXME: this implementation will not detect executables that cross bank
|
||||
// boundaries (but it shouldn't matter as executables must be <4 MB anyway)
|
||||
auto data = reinterpret_cast<const uint8_t *>(ptr + FLASH_EXECUTABLE_OFFSET);
|
||||
@ -185,10 +201,10 @@ bool FlashRegion::hasBootExecutable(void) const {
|
||||
|
||||
io::setFlashBank(bank);
|
||||
|
||||
auto &header = *reinterpret_cast<const util::ExecutableHeader *>(data);
|
||||
auto header = reinterpret_cast<const util::ExecutableHeader *>(data);
|
||||
|
||||
if (!header.validateMagic())
|
||||
return false;
|
||||
if (!header->validateMagic())
|
||||
return nullptr;
|
||||
|
||||
// The integrity of the executable is verified by calculating the CRC32 of
|
||||
// its bytes whose offsets are powers of 2 (i.e. the bytes at indices 0, 1,
|
||||
@ -196,7 +212,7 @@ bool FlashRegion::hasBootExecutable(void) const {
|
||||
// header.textLength + util::EXECUTABLE_BODY_OFFSET, as the CRC is also
|
||||
// calculated on the header, but Konami's shell ignores the last 2048 bytes
|
||||
// due to a bug.
|
||||
size_t length = header.textLength;
|
||||
size_t length = header->textLength;
|
||||
uint32_t crc = ~0;
|
||||
|
||||
crc = (crc >> 8) ^ table[(crc ^ *data) & 0xff];
|
||||
@ -204,7 +220,10 @@ bool FlashRegion::hasBootExecutable(void) const {
|
||||
for (size_t i = 1; i < length; i <<= 1)
|
||||
crc = (crc >> 8) ^ table[(crc ^ data[i]) & 0xff];
|
||||
|
||||
return (~crc == *crcPtr);
|
||||
if (~crc != *crcPtr)
|
||||
return nullptr;
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
uint32_t FlashRegion::getJEDECID(void) const {
|
||||
|
@ -32,7 +32,9 @@ public:
|
||||
uint32_t offset, size_t length, uint32_t crc = 0
|
||||
) const;
|
||||
|
||||
virtual bool hasBootExecutable(void) const { return false; }
|
||||
virtual const util::ExecutableHeader *getBootExecutableHeader(void) const {
|
||||
return nullptr;
|
||||
}
|
||||
virtual uint32_t getJEDECID(void) const { return 0; }
|
||||
virtual Driver *newDriver(void) const { return nullptr; }
|
||||
};
|
||||
@ -67,7 +69,7 @@ public:
|
||||
void read(void *data, uint32_t offset, size_t length) const;
|
||||
uint32_t zipCRC32(uint32_t offset, size_t length, uint32_t crc = 0) const;
|
||||
|
||||
bool hasBootExecutable(void) const;
|
||||
const util::ExecutableHeader *getBootExecutableHeader(void) const;
|
||||
uint32_t getJEDECID(void) const;
|
||||
Driver *newDriver(void) const;
|
||||
};
|
||||
|
@ -7,30 +7,42 @@
|
||||
|
||||
extern "C" uint8_t _textStart[];
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
io::init();
|
||||
static constexpr size_t _LOAD_CHUNK_LENGTH = 0x8000;
|
||||
|
||||
args::ExecutableLauncherArgs args;
|
||||
static int _loadFromFlash(args::ExecutableLauncherArgs &args) {
|
||||
io::setFlashBank(args.device);
|
||||
|
||||
for (; argc > 0; argc--)
|
||||
args.parseArgument(*(argv++));
|
||||
// The executable's offset and length are always passed as a single
|
||||
// fragment.
|
||||
auto ptr = reinterpret_cast<uintptr_t>(args.loadAddress);
|
||||
auto source = uintptr_t(args.fragments[0].lba);
|
||||
auto length = size_t(args.fragments[0].length);
|
||||
|
||||
#if defined(ENABLE_APP_LOGGING) || defined(ENABLE_IDE_LOGGING)
|
||||
util::logger.setupSyslog(args.baudRate);
|
||||
#endif
|
||||
while (length) {
|
||||
size_t chunkLength = util::min(length, _LOAD_CHUNK_LENGTH);
|
||||
|
||||
if (!args.entryPoint || !args.loadAddress || !args.numFragments) {
|
||||
LOG_APP("required arguments missing");
|
||||
return 1;
|
||||
__builtin_memcpy(
|
||||
reinterpret_cast<void *>(ptr),
|
||||
reinterpret_cast<const void *>(source), chunkLength
|
||||
);
|
||||
io::clearWatchdog();
|
||||
|
||||
ptr += chunkLength;
|
||||
source += chunkLength;
|
||||
length -= chunkLength;
|
||||
}
|
||||
|
||||
if (!args.stackTop)
|
||||
args.stackTop = _textStart - 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto &dev = ide::devices[args.drive];
|
||||
static int _loadFromIDE(args::ExecutableLauncherArgs &args) {
|
||||
int drive = -(args.device + 1);
|
||||
auto &dev = ide::devices[drive];
|
||||
|
||||
if (dev.enumerate()) {
|
||||
LOG_APP("drive %d initialization failed", args.drive);
|
||||
auto error = dev.enumerate();
|
||||
|
||||
if (error) {
|
||||
LOG_APP("drive %d: %s", drive, ide::getErrorString(error));
|
||||
return 2;
|
||||
}
|
||||
|
||||
@ -57,8 +69,10 @@ int main(int argc, const char **argv) {
|
||||
length -= skipSectors;
|
||||
}
|
||||
|
||||
if (dev.readData(reinterpret_cast<void *>(ptr), lba, length)) {
|
||||
LOG_APP("read failed, lba=0x%08x", lba);
|
||||
error = dev.readData(reinterpret_cast<void *>(ptr), lba, length);
|
||||
|
||||
if (error) {
|
||||
LOG_APP("drive %d: %s", drive, ide::getErrorString(error));
|
||||
return 3;
|
||||
}
|
||||
|
||||
@ -66,6 +80,36 @@ int main(int argc, const char **argv) {
|
||||
ptr += length * sectorSize;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
io::init();
|
||||
|
||||
args::ExecutableLauncherArgs args;
|
||||
|
||||
for (; argc > 0; argc--)
|
||||
args.parseArgument(*(argv++));
|
||||
|
||||
#if defined(ENABLE_APP_LOGGING) || defined(ENABLE_IDE_LOGGING)
|
||||
util::logger.setupSyslog(args.baudRate);
|
||||
#endif
|
||||
|
||||
if (!args.entryPoint || !args.loadAddress || !args.numFragments) {
|
||||
LOG_APP("required arguments missing");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!args.stackTop)
|
||||
args.stackTop = _textStart - 16;
|
||||
|
||||
int error = (args.device >= 0)
|
||||
? _loadFromFlash(args)
|
||||
: _loadFromIDE(args);
|
||||
|
||||
if (error)
|
||||
return error;
|
||||
|
||||
// Launch the executable.
|
||||
util::ExecutableLoader loader(
|
||||
args.entryPoint, args.initialGP, args.stackTop
|
||||
|
@ -78,7 +78,7 @@ FileIOManager::FileIOManager(void)
|
||||
void FileIOManager::initIDE(void) {
|
||||
closeIDE();
|
||||
|
||||
char name[6]{ "ide#:" };
|
||||
char name[8]{ "ide#:\0" };
|
||||
|
||||
for (size_t i = 0; i < util::countOf(ide::devices); i++) {
|
||||
auto &dev = ide::devices[i];
|
||||
@ -117,7 +117,7 @@ void FileIOManager::initIDE(void) {
|
||||
}
|
||||
|
||||
void FileIOManager::closeIDE(void) {
|
||||
char name[6]{ "ide#:" };
|
||||
char name[8]{ "ide#:\0" };
|
||||
|
||||
for (size_t i = 0; i < util::countOf(ide::devices); i++) {
|
||||
if (ide[i]) {
|
||||
|
@ -41,7 +41,7 @@ bool App::_ideInitWorker(void) {
|
||||
#ifdef ENABLE_AUTOBOOT
|
||||
// Only try to autoboot if DIP switch 1 is on.
|
||||
if (io::getDIPSwitch(0)) {
|
||||
_workerStatus.update(3, 4, WSTR("App.fileInitWorker.autoboot"));
|
||||
_workerStatus.update(3, 4, WSTR("App.ideInitWorker.autoboot"));
|
||||
|
||||
for (auto path : _AUTOBOOT_PATHS) {
|
||||
file::FileInfo info;
|
||||
@ -62,14 +62,14 @@ bool App::_ideInitWorker(void) {
|
||||
}
|
||||
|
||||
bool App::_fileInitWorker(void) {
|
||||
_workerStatus.update(0, 4, WSTR("App.fileInitWorker.unmount"));
|
||||
_workerStatus.update(0, 3, WSTR("App.fileInitWorker.unmount"));
|
||||
_fileIO.closeResourceFile();
|
||||
_fileIO.close();
|
||||
|
||||
_workerStatus.update(1, 4, WSTR("App.fileInitWorker.mount"));
|
||||
_workerStatus.update(1, 3, WSTR("App.fileInitWorker.mount"));
|
||||
_fileIO.initIDE();
|
||||
|
||||
_workerStatus.update(2, 4, WSTR("App.fileInitWorker.loadResources"));
|
||||
_workerStatus.update(2, 3, WSTR("App.fileInitWorker.loadResources"));
|
||||
if (_fileIO.loadResourceFile(EXTERNAL_DATA_DIR "/resource.zip"))
|
||||
_loadResources();
|
||||
|
||||
|
@ -224,7 +224,7 @@ void FilePickerScreen::update(ui::Context &ctx) {
|
||||
}
|
||||
#endif
|
||||
|
||||
char name[6]{ "ide#:" };
|
||||
char name[8]{ "ide#:\0" };
|
||||
|
||||
int drive = _drives[index];
|
||||
auto &dev = ide::devices[drive];
|
||||
|
@ -72,7 +72,7 @@ void StorageInfoScreen::show(ui::Context &ctx, bool goBack) {
|
||||
(id >> 24) & 0xff
|
||||
);
|
||||
|
||||
if (rom::flash.hasBootExecutable())
|
||||
if (rom::flash.getBootExecutableHeader())
|
||||
_PRINT(STR("StorageInfoScreen.flash.bootable"));
|
||||
|
||||
// TODO: show information about currently installed game
|
||||
@ -96,7 +96,7 @@ void StorageInfoScreen::show(ui::Context &ctx, bool goBack) {
|
||||
(id >> 24) & 0xff
|
||||
);
|
||||
|
||||
if (card.hasBootExecutable())
|
||||
if (card.getBootExecutableHeader())
|
||||
_PRINT(STR("StorageInfoScreen.pcmcia.bootable"));
|
||||
} else {
|
||||
_PRINT(STR("StorageInfoScreen.pcmcia.noCard"));
|
||||
|
Loading…
x
Reference in New Issue
Block a user