Bump to 0.4.5, add flash executable loading, fix bugs

This commit is contained in:
spicyjpeg 2024-06-05 18:50:00 +02:00
parent aac5f4abb1
commit f4a8d16b20
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
17 changed files with 177 additions and 78 deletions

View File

@ -6,7 +6,7 @@ set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
project( project(
cart-tool cart-tool
LANGUAGES C CXX ASM LANGUAGES C CXX ASM
VERSION 0.4.4 VERSION 0.4.5
DESCRIPTION "Konami System 573 security cartridge tool" DESCRIPTION "Konami System 573 security cartridge tool"
) )

View File

@ -7,8 +7,8 @@
extern "C" const uint8_t _resourceArchive[]; extern "C" const uint8_t _resourceArchive[];
extern "C" const size_t _resourceArchiveLength; extern "C" const size_t _resourceArchiveLength;
static char _ptrArg[]{ "resource.ptr=xxxxxxxx" }; static char _ptrArg[]{ "resource.ptr=xxxxxxxx\0" };
static char _lengthArg[]{ "resource.length=xxxxxxxx" }; static char _lengthArg[]{ "resource.length=xxxxxxxx\0" };
struct [[gnu::packed]] ZIPFileHeader { struct [[gnu::packed]] ZIPFileHeader {
public: public:

View File

@ -87,8 +87,8 @@ bool ExecutableLauncherArgs::parseArgument(const char *arg) {
loadAddress = reinterpret_cast<void *>(strtol(&arg[5], nullptr, 16)); loadAddress = reinterpret_cast<void *>(strtol(&arg[5], nullptr, 16));
return true; return true;
case "drive"_h: case "device"_h:
drive = int(strtol(&arg[6], nullptr, 0)); device = int(strtol(&arg[6], nullptr, 0));
return true; return true;
case "frag"_h: case "frag"_h:

View File

@ -52,7 +52,7 @@ public:
void *entryPoint, *initialGP, *stackTop; void *entryPoint, *initialGP, *stackTop;
void *loadAddress; void *loadAddress;
int drive; int device; // 0-63 = flash, -1 or -2 = IDE
size_t numArgs, numFragments; size_t numArgs, numFragments;
const char *executableArgs[util::MAX_EXECUTABLE_ARGS]; const char *executableArgs[util::MAX_EXECUTABLE_ARGS];
@ -60,7 +60,7 @@ public:
inline ExecutableLauncherArgs(void) inline ExecutableLauncherArgs(void)
: entryPoint(nullptr), initialGP(nullptr), stackTop(nullptr), : 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); bool parseArgument(const char *arg);
}; };

View File

@ -104,6 +104,9 @@ void FATDirectory::close(void) {
/* FAT filesystem provider */ /* FAT filesystem provider */
bool FATProvider::init(int drive) { bool FATProvider::init(int drive) {
if (type)
return false;
_drive[0] = drive + '0'; _drive[0] = drive + '0';
auto error = f_mount(&_fs, _drive, 1); auto error = f_mount(&_fs, _drive, 1);
@ -123,6 +126,9 @@ bool FATProvider::init(int drive) {
} }
void FATProvider::close(void) { void FATProvider::close(void) {
if (!type)
return;
auto error = f_unmount(_drive); auto error = f_unmount(_drive);
if (error) { if (error) {

View File

@ -20,11 +20,12 @@ enum FileSystemType {
FAT12 = 1, FAT12 = 1,
FAT16 = 2, FAT16 = 2,
FAT32 = 3, FAT32 = 3,
ISO9660 = 4, EXFAT = 4,
ZIP_MEMORY = 5, ISO9660 = 5,
ZIP_FILE = 6, ZIP_MEMORY = 6,
HOST = 7, ZIP_FILE = 7,
VFS = 8 HOST = 8,
VFS = 9
}; };
// These are functionally equivalent to the FA_* flags used by FatFs. // These are functionally equivalent to the FA_* flags used by FatFs.

View File

@ -180,24 +180,33 @@ VFSMountPoint *VFSProvider::_getMounted(const char *path) {
bool VFSProvider::mount(const char *prefix, Provider *provider, bool force) { bool VFSProvider::mount(const char *prefix, Provider *provider, bool force) {
auto hash = util::hash(prefix, VFS_PREFIX_SEPARATOR); auto hash = util::hash(prefix, VFS_PREFIX_SEPARATOR);
VFSMountPoint *freeMP = nullptr;
for (auto &mp : _mountPoints) { for (auto &mp : _mountPoints) {
if (force) { if (!mp.prefix) {
if (mp.prefix && (mp.prefix != hash)) freeMP = &mp;
continue; } else if (mp.prefix == hash) {
} else { if (force) {
if (mp.prefix) freeMP = &mp;
continue; 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) { bool VFSProvider::unmount(const char *prefix) {
@ -215,6 +224,7 @@ bool VFSProvider::unmount(const char *prefix) {
return true; return true;
} }
LOG_FS("%s was not mapped", prefix);
return false; return false;
} }

View File

@ -97,6 +97,9 @@ static constexpr uint32_t _ZIP_FLAGS = 0
| MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY; | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY;
bool ZIPProvider::init(File *file) { bool ZIPProvider::init(File *file) {
if (type)
return false;
mz_zip_zero_struct(&_zip); mz_zip_zero_struct(&_zip);
_file = file; _file = file;
@ -128,6 +131,9 @@ bool ZIPProvider::init(File *file) {
} }
bool ZIPProvider::init(const void *zipData, size_t length) { bool ZIPProvider::init(const void *zipData, size_t length) {
if (type)
return false;
mz_zip_zero_struct(&_zip); mz_zip_zero_struct(&_zip);
_file = nullptr; _file = nullptr;
@ -146,6 +152,9 @@ bool ZIPProvider::init(const void *zipData, size_t length) {
} }
void ZIPProvider::close(void) { void ZIPProvider::close(void) {
if (!type)
return;
mz_zip_reader_end(&_zip); mz_zip_reader_end(&_zip);
#if 0 #if 0

View File

@ -607,8 +607,9 @@ DeviceError Device::enumerate(void) {
// actually present. A strict timeout is used in the commands below in order // actually present. A strict timeout is used in the commands below in order
// to prevent blocking for too long. // to prevent blocking for too long.
IdentifyBlock block; IdentifyBlock block;
auto signature = _getCylinder();
if (_getCylinder() == _ATAPI_SIGNATURE) { if (signature == _ATAPI_SIGNATURE) {
flags |= DEVICE_ATAPI; flags |= DEVICE_ATAPI;
_write(CS0_COMMAND, ATA_IDENTIFY_PACKET); _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. // Find out the fastest PIO transfer mode supported and enable it.
int mode = block.getHighestPIOMode(); int mode = block.getHighestPIOMode();
_select(0);
error = _waitForIdle();
if (error)
return error;
_write(CS0_FEATURES, FEATURE_TRANSFER_MODE); _write(CS0_FEATURES, FEATURE_TRANSFER_MODE);
_write(CS0_COUNT, TRANSFER_MODE_PIO | mode); _write(CS0_COUNT, TRANSFER_MODE_PIO | mode);
_write(CS0_COMMAND, ATA_SET_FEATURES); _write(CS0_COMMAND, ATA_SET_FEATURES);

View File

@ -36,12 +36,12 @@ void init(void) {
| ( 4 << 24) // DMA read/write delay | ( 4 << 24) // DMA read/write delay
| BIU_CTRL_DMA_DELAY; | BIU_CTRL_DMA_DELAY;
#if 0
// Revision D of the main board has footprints for either eight 8-bit RAM // 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 // 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 // bank.
// appropriately, but this makes sure the configuration is correct if e.g.
// the tool is booted through OpenBIOS instead.
DRAM_CTRL = isDualBankRAM() ? 0x0c80 : 0x4788; DRAM_CTRL = isDualBankRAM() ? 0x0c80 : 0x4788;
#endif
_bankSwitchReg = 0; _bankSwitchReg = 0;
_cartOutputReg = 0; _cartOutputReg = 0;

View File

@ -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 source = reinterpret_cast<const uint32_t *>(ptr + offset);
auto dest = reinterpret_cast<uint32_t *>(data); 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>(source);
util::assertAligned<uint32_t>(dest); 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) for (; length; length -= 4)
*(dest++) = *(source++); *(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 // 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. // 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--) for (; length; length--)
*(dest++) = *(source++) & 0xff; *(dest++) = uint8_t(*(source++));
} }
uint32_t RTCRegion::zipCRC32( 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 { void FlashRegion::read(void *data, uint32_t offset, size_t length) const {
// FIXME: this implementation will not handle unaligned reads and reads that // FIXME: this implementation will not handle unaligned reads and reads that
// cross bank boundaries properly // cross bank boundaries properly
int bankOffset = offset / FLASH_BANK_LENGTH; auto bankOffset = offset / FLASH_BANK_LENGTH;
int ptrOffset = 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); io::setFlashBank(bank + bankOffset);
Region::read(data, ptrOffset, length);
for (; length; length -= 4)
*(dest++) = *(source++);
} }
uint32_t FlashRegion::zipCRC32( uint32_t FlashRegion::zipCRC32(
@ -139,8 +155,8 @@ uint32_t FlashRegion::zipCRC32(
) const { ) const {
// FIXME: this implementation will not handle unaligned reads and reads that // FIXME: this implementation will not handle unaligned reads and reads that
// cross bank boundaries properly // cross bank boundaries properly
int bankOffset = offset / FLASH_BANK_LENGTH; auto bankOffset = offset / FLASH_BANK_LENGTH;
int ptrOffset = offset % FLASH_BANK_LENGTH; auto ptrOffset = offset % FLASH_BANK_LENGTH;
auto source = reinterpret_cast<const uint32_t *>(ptr + ptrOffset); auto source = reinterpret_cast<const uint32_t *>(ptr + ptrOffset);
auto table = reinterpret_cast<const uint32_t *>(CACHE_BASE); auto table = reinterpret_cast<const uint32_t *>(CACHE_BASE);
@ -176,7 +192,7 @@ enum FlashIdentifier : uint16_t {
_ID_28F640J5 = 0x89 | (0x15 << 8) _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 // FIXME: this implementation will not detect executables that cross bank
// boundaries (but it shouldn't matter as executables must be <4 MB anyway) // boundaries (but it shouldn't matter as executables must be <4 MB anyway)
auto data = reinterpret_cast<const uint8_t *>(ptr + FLASH_EXECUTABLE_OFFSET); auto data = reinterpret_cast<const uint8_t *>(ptr + FLASH_EXECUTABLE_OFFSET);
@ -185,10 +201,10 @@ bool FlashRegion::hasBootExecutable(void) const {
io::setFlashBank(bank); io::setFlashBank(bank);
auto &header = *reinterpret_cast<const util::ExecutableHeader *>(data); auto header = reinterpret_cast<const util::ExecutableHeader *>(data);
if (!header.validateMagic()) if (!header->validateMagic())
return false; return nullptr;
// The integrity of the executable is verified by calculating the CRC32 of // 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, // 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 // header.textLength + util::EXECUTABLE_BODY_OFFSET, as the CRC is also
// calculated on the header, but Konami's shell ignores the last 2048 bytes // calculated on the header, but Konami's shell ignores the last 2048 bytes
// due to a bug. // due to a bug.
size_t length = header.textLength; size_t length = header->textLength;
uint32_t crc = ~0; uint32_t crc = ~0;
crc = (crc >> 8) ^ table[(crc ^ *data) & 0xff]; 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) for (size_t i = 1; i < length; i <<= 1)
crc = (crc >> 8) ^ table[(crc ^ data[i]) & 0xff]; crc = (crc >> 8) ^ table[(crc ^ data[i]) & 0xff];
return (~crc == *crcPtr); if (~crc != *crcPtr)
return nullptr;
return header;
} }
uint32_t FlashRegion::getJEDECID(void) const { uint32_t FlashRegion::getJEDECID(void) const {

View File

@ -32,7 +32,9 @@ public:
uint32_t offset, size_t length, uint32_t crc = 0 uint32_t offset, size_t length, uint32_t crc = 0
) const; ) 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 uint32_t getJEDECID(void) const { return 0; }
virtual Driver *newDriver(void) const { return nullptr; } virtual Driver *newDriver(void) const { return nullptr; }
}; };
@ -67,7 +69,7 @@ public:
void read(void *data, uint32_t offset, size_t length) const; 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; 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; uint32_t getJEDECID(void) const;
Driver *newDriver(void) const; Driver *newDriver(void) const;
}; };

View File

@ -7,30 +7,42 @@
extern "C" uint8_t _textStart[]; extern "C" uint8_t _textStart[];
int main(int argc, const char **argv) { static constexpr size_t _LOAD_CHUNK_LENGTH = 0x8000;
io::init();
args::ExecutableLauncherArgs args; static int _loadFromFlash(args::ExecutableLauncherArgs &args) {
io::setFlashBank(args.device);
for (; argc > 0; argc--) // The executable's offset and length are always passed as a single
args.parseArgument(*(argv++)); // 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) while (length) {
util::logger.setupSyslog(args.baudRate); size_t chunkLength = util::min(length, _LOAD_CHUNK_LENGTH);
#endif
if (!args.entryPoint || !args.loadAddress || !args.numFragments) { __builtin_memcpy(
LOG_APP("required arguments missing"); reinterpret_cast<void *>(ptr),
return 1; reinterpret_cast<const void *>(source), chunkLength
);
io::clearWatchdog();
ptr += chunkLength;
source += chunkLength;
length -= chunkLength;
} }
if (!args.stackTop) return 0;
args.stackTop = _textStart - 16; }
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()) { auto error = dev.enumerate();
LOG_APP("drive %d initialization failed", args.drive);
if (error) {
LOG_APP("drive %d: %s", drive, ide::getErrorString(error));
return 2; return 2;
} }
@ -57,8 +69,10 @@ int main(int argc, const char **argv) {
length -= skipSectors; length -= skipSectors;
} }
if (dev.readData(reinterpret_cast<void *>(ptr), lba, length)) { error = dev.readData(reinterpret_cast<void *>(ptr), lba, length);
LOG_APP("read failed, lba=0x%08x", lba);
if (error) {
LOG_APP("drive %d: %s", drive, ide::getErrorString(error));
return 3; return 3;
} }
@ -66,6 +80,36 @@ int main(int argc, const char **argv) {
ptr += length * sectorSize; 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. // Launch the executable.
util::ExecutableLoader loader( util::ExecutableLoader loader(
args.entryPoint, args.initialGP, args.stackTop args.entryPoint, args.initialGP, args.stackTop

View File

@ -78,7 +78,7 @@ FileIOManager::FileIOManager(void)
void FileIOManager::initIDE(void) { void FileIOManager::initIDE(void) {
closeIDE(); closeIDE();
char name[6]{ "ide#:" }; char name[8]{ "ide#:\0" };
for (size_t i = 0; i < util::countOf(ide::devices); i++) { for (size_t i = 0; i < util::countOf(ide::devices); i++) {
auto &dev = ide::devices[i]; auto &dev = ide::devices[i];
@ -117,7 +117,7 @@ void FileIOManager::initIDE(void) {
} }
void FileIOManager::closeIDE(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++) { for (size_t i = 0; i < util::countOf(ide::devices); i++) {
if (ide[i]) { if (ide[i]) {

View File

@ -41,7 +41,7 @@ bool App::_ideInitWorker(void) {
#ifdef ENABLE_AUTOBOOT #ifdef ENABLE_AUTOBOOT
// Only try to autoboot if DIP switch 1 is on. // Only try to autoboot if DIP switch 1 is on.
if (io::getDIPSwitch(0)) { 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) { for (auto path : _AUTOBOOT_PATHS) {
file::FileInfo info; file::FileInfo info;
@ -62,14 +62,14 @@ bool App::_ideInitWorker(void) {
} }
bool App::_fileInitWorker(void) { bool App::_fileInitWorker(void) {
_workerStatus.update(0, 4, WSTR("App.fileInitWorker.unmount")); _workerStatus.update(0, 3, WSTR("App.fileInitWorker.unmount"));
_fileIO.closeResourceFile(); _fileIO.closeResourceFile();
_fileIO.close(); _fileIO.close();
_workerStatus.update(1, 4, WSTR("App.fileInitWorker.mount")); _workerStatus.update(1, 3, WSTR("App.fileInitWorker.mount"));
_fileIO.initIDE(); _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")) if (_fileIO.loadResourceFile(EXTERNAL_DATA_DIR "/resource.zip"))
_loadResources(); _loadResources();

View File

@ -224,7 +224,7 @@ void FilePickerScreen::update(ui::Context &ctx) {
} }
#endif #endif
char name[6]{ "ide#:" }; char name[8]{ "ide#:\0" };
int drive = _drives[index]; int drive = _drives[index];
auto &dev = ide::devices[drive]; auto &dev = ide::devices[drive];

View File

@ -72,7 +72,7 @@ void StorageInfoScreen::show(ui::Context &ctx, bool goBack) {
(id >> 24) & 0xff (id >> 24) & 0xff
); );
if (rom::flash.hasBootExecutable()) if (rom::flash.getBootExecutableHeader())
_PRINT(STR("StorageInfoScreen.flash.bootable")); _PRINT(STR("StorageInfoScreen.flash.bootable"));
// TODO: show information about currently installed game // TODO: show information about currently installed game
@ -96,7 +96,7 @@ void StorageInfoScreen::show(ui::Context &ctx, bool goBack) {
(id >> 24) & 0xff (id >> 24) & 0xff
); );
if (card.hasBootExecutable()) if (card.getBootExecutableHeader())
_PRINT(STR("StorageInfoScreen.pcmcia.bootable")); _PRINT(STR("StorageInfoScreen.pcmcia.bootable"));
} else { } else {
_PRINT(STR("StorageInfoScreen.pcmcia.noCard")); _PRINT(STR("StorageInfoScreen.pcmcia.noCard"));