Add support for Slot 2 flashcarts using Compact Flash (#84)

- Supercard CF (SUPERCARDCF)
- GBA Media Player CF (MPCF)
- M3 Adapter CF (M3CF)
- Max Media Dock CF (MMCF)
This commit is contained in:
Edoardo Lolletti
2026-01-10 23:00:39 +01:00
committed by GitHub
parent eac8f7e734
commit 6a97b677a7
22 changed files with 945 additions and 3 deletions

View File

@@ -0,0 +1,66 @@
#include "common.h"
#include <libtwl/mem/memExtern.h>
#include "CompactFlashCommonLoaderPlatform.h"
static constexpr int CF_CARD_TIMEOUT = 10000000;
static constexpr int CF_STS_READY = 0x40;
static constexpr int CF_STS_DSC = 0x10;
static constexpr int CF_STS_BUSY = 0x80;
bool CompactFlashCommonLoaderPlatform::InitializeSdCard()
{
u32 oldMemCnt = REG_EXMEMCNT;
mem_setGbaCartridgeRomWaits(EXMEMCNT_SLOT2_ROM_WAIT1_10, EXMEMCNT_SLOT2_ROM_WAIT2_6);
SetCardLocked(false);
auto res = InitializeCfCard();
SetCardLocked(true);
REG_EXMEMCNT = oldMemCnt;
return res;
}
static bool waitAvailableForCommands(const cf_registers_t& registers)
{
// wait for card to finish previous commands
for (int i = 0; i < CF_CARD_TIMEOUT; i++)
{
if ((*registers.command & CF_STS_BUSY) == 0)
{
break;
}
}
// wait for card to be ready for new commands
for (int i = 0; i < CF_CARD_TIMEOUT; i++)
{
if ((*registers.command & (CF_STS_READY | CF_STS_DSC)) != 0)
{
return true;
}
}
return false;
}
bool CompactFlashCommonLoaderPlatform::InitializeCfCard()
{
const auto& registers = GetCfRegisters();
if (!waitAvailableForCommands(registers))
{
return false;
}
// Check that the registers are writable and hold the values we set
u16 temp = *registers.lba1;
*registers.lba1 = (~temp & 0xFF);
temp = (~temp & 0xFF);
if (*registers.lba1 != temp)
{
return false;
}
*registers.lba1 = 0xAA55;
if (*registers.lba1 == 0xAA55)
{
return false;
}
return true;
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include "common.h"
#include "../LoaderPlatform.h"
#include "CompactFlashRegisters.h"
#include "CompactFlashStatusFunctionsPatchCode.h"
#include "CompactFlashReadWriteSectorPatchCode.h"
#include "ICompactFlashLockUnlockPatchCode.h"
/// @brief Base implementation of LoaderPlatform for the Compact Flash slot 2 flashcarts
class CompactFlashCommonLoaderPlatform : public LoaderPlatform
{
public:
LoaderPlatformType GetPlatformType() const override { return LoaderPlatformType::Slot2; }
bool InitializeSdCard() override;
const IReadSectorsPatchCode* CreateSdReadPatchCode(
PatchCodeCollection& patchCodeCollection, PatchHeap& patchHeap) const override
{
const auto& registers = GetCfRegisters();
auto statusFunctions = patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashStatusFunctionsPatchCode(patchHeap, registers);
});
auto transferSector = patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashTransferSectorPatchCode(patchHeap, registers, statusFunctions);
});
auto lockUnlock = CreateLockingPatchCode(patchCodeCollection, patchHeap);
return patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashReadWriteSectorPatchCode(patchHeap, registers, transferSector, lockUnlock);
});
}
const IWriteSectorsPatchCode* CreateSdWritePatchCode(
PatchCodeCollection& patchCodeCollection, PatchHeap& patchHeap) const override
{
const auto& registers = GetCfRegisters();
auto statusFunctions = patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashStatusFunctionsPatchCode(patchHeap, registers);
});
auto transferSector = patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashTransferSectorPatchCode(patchHeap, registers, statusFunctions);
});
auto lockUnlock = CreateLockingPatchCode(patchCodeCollection, patchHeap);
return patchCodeCollection.GetOrAddSharedPatchCode([&]
{
return new CompactFlashReadWriteSectorPatchCode(patchHeap, registers, transferSector, lockUnlock);
});
}
protected:
/// @brief Locks/Unlocks the cart to operate on the inserted CF card
/// @param locked Whether the card should be locked (prevent R/W operations) or unlocked (allows R/W operations)
virtual void SetCardLocked(bool locked) const = 0;
/// @brief Generates the patch code containing the lock/unlock routines equivalent to \see SetCardLocked
/// If a card requires no locking, this doesn't have to be implemented.
/// @note The returned routine, is only allowed to modify r0, other registers should be left untouched
/// @return A pointer to the allocated \see ICompactFlashLockUnlockPatchCode
virtual const ICompactFlashLockUnlockPatchCode* CreateLockingPatchCode(
PatchCodeCollection& patchCodeCollection, PatchHeap& patchHeap) const { return nullptr; }
/// @brief Returns the exposed address associated to the Compact Flash registers
/// @return the \see cf_registers_t struct containing the registers
virtual const cf_registers_t& GetCfRegisters() const = 0;
private:
bool InitializeCfCard();
};

View File

@@ -0,0 +1,86 @@
#pragma once
#include "common.h"
#include "sections.h"
#include "patches/PatchCode.h"
#include "thumbInstructions.h"
#include "../IReadSectorsPatchCode.h"
#include "../IWriteSectorsPatchCode.h"
#include "CompactFlashRegisters.h"
#include "ICompactFlashLockUnlockPatchCode.h"
DEFINE_SECTION_SYMBOLS(cf_perform_transfer);
DEFINE_SECTION_SYMBOLS(cf_read_write_functions);
extern "C" bool cf_performTransferSectors(u32 numSectors, u32 sector, u8 command, void* srcAddr, void* dstAddr);
extern "C" bool cf_readSectors(u32 sector, void* buffer, u32 numSectors);
extern "C" bool cf_writeSectors(u32 sector, void* buffer, u32 numSectors);
extern vu16* cf_performTransferSectors_reg_sector_count;
extern u32 cf_performTransferSectors_waitCardAvailableForCommands;
extern u32 cf_performTransferSectors_waitNextBlockReady;
extern vu16* cf_performTransfer_reg_data;
extern u32 cf_performTransfer_performTransferSectors;
extern u32 cf_performTransfer_lockUnlockCard;
extern u16 cf_performTransfer_unlock_label[2];
extern u16 cf_performTransfer_lock_label[2];
class CompactFlashTransferSectorPatchCode : public PatchCode
{
public:
CompactFlashTransferSectorPatchCode(PatchHeap& patchHeap,
const cf_registers_t& registers,
const CompactFlashStatusFunctionsPatchCode* compactFlashStatusFunctionsPatchCode)
: PatchCode(SECTION_START(cf_perform_transfer), SECTION_SIZE(cf_perform_transfer), patchHeap)
{
cf_performTransferSectors_reg_sector_count = registers.sectorCount;
cf_performTransferSectors_waitCardAvailableForCommands = (u32)compactFlashStatusFunctionsPatchCode->GetWaitCardAvailableForCommandsFunction();
cf_performTransferSectors_waitNextBlockReady = (u32)compactFlashStatusFunctionsPatchCode->GetWaitNextBlockReadyFunction();
}
const void* GetPerformTransferSectorsFunction() const
{
return GetAddressAtTarget((void*)cf_performTransferSectors);
}
};
class CompactFlashReadWriteSectorPatchCode : public PatchCode, public IReadSectorsPatchCode, public IWriteSectorsPatchCode
{
public:
CompactFlashReadWriteSectorPatchCode(PatchHeap& patchHeap,
const cf_registers_t& registers,
const CompactFlashTransferSectorPatchCode* compactFlashTransferSectorPatchCode,
const ICompactFlashLockUnlockPatchCode* lockUnlockCard)
: PatchCode(SECTION_START(cf_read_write_functions), SECTION_SIZE(cf_read_write_functions), patchHeap)
{
cf_performTransfer_reg_data = registers.data;
cf_performTransfer_performTransferSectors = (u32)compactFlashTransferSectorPatchCode->GetPerformTransferSectorsFunction();
if (lockUnlockCard)
{
cf_performTransfer_lockUnlockCard = (u32)lockUnlockCard->GetLockUnlockFunction();
}
else
{
// what is getting replaced is a `bl`, taking 4 bytes
const u16 noLockingOpcode = THUMB_MOV_HIREG(THUMB_HI_R8, THUMB_HI_R8);
cf_performTransfer_unlock_label[0] = noLockingOpcode;
cf_performTransfer_unlock_label[1] = noLockingOpcode;
cf_performTransfer_lock_label[0] = noLockingOpcode;
cf_performTransfer_lock_label[1] = noLockingOpcode;
}
}
const ReadSectorsFunc GetReadSectorsFunction() const override
{
return (const ReadSectorsFunc)GetAddressAtTarget((void*)cf_readSectors);
}
const WriteSectorsFunc GetWriteSectorFunction() const override
{
return (const WriteSectorsFunc)GetAddressAtTarget((void*)cf_writeSectors);
}
};

View File

@@ -0,0 +1,202 @@
.cpu arm7tdmi
.syntax unified
.thumb
.section "cf_perform_transfer", "ax"
.equ CF_CMD_LBA, 0xE0
.equ CF_CMD_READ, 0x20
.equ CF_CMD_WRITE, 0x30
@ bool cf_performTransferSectors(u32 numSectors, u32 sector, void* srcAddr, void* dstAddr, u8 command)
.type cf_performTransferSectors, %function
.global cf_performTransferSectors
cf_performTransferSectors:
push {r0,r1,r4-r7,lr}
ldr r7, cf_performTransferSectors_waitCardAvailableForCommands
@ calls waitCardAvailableForCommands
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
bl cf_performTransferSectors_interwork
beq cf_performTransferSectors_error
ldr r5, cf_performTransferSectors_reg_sector_count
@ load 0x20000
movs r6, #0x01
lsls r6, #17
@ store sector count
strh r0, [r5]
adds r5, r6
lsls r7, r1, #24
lsrs r7, r7, #24
@ store lba1
strh r7, [r5]
adds r5, r6
lsls r7, r1, #16
lsrs r7, r7, #24
@ store lba2
strh r7, [r5]
adds r5, r6
lsls r7, r1, #8
lsrs r7, r7, #24
@ store lba3
strh r7, [r5]
adds r5, r6
@ Only lower nibble is transferred
lsls r7, r1, #4
lsrs r7, r7, #28
@ store lba4
adds r7, CF_CMD_LBA
strh r7, [r5]
@ store command
strh r4, [r5, r6]
@ get total number of bytes to write
lsls r0, #9
ldr r7, cf_performTransferSectors_waitNextBlockReady
read_next_block:
@ calls waitCardNextBlockReady
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
bl cf_performTransferSectors_interwork
beq cf_performTransferSectors_error
read_next_int:
ldm r2!, {r1,r4,r5,r6}
stm r3!, {r1,r4,r5,r6}
subs r0, #16
beq done
@ Shifting left by 0x17 will set the Zero flag if the number that was shifted is a multiple
@ of 0x200 (indicating a full sector has been written)
lsls r1, r0, #0x17
bne read_next_int
b read_next_block
done:
movs r0, #1
cf_performTransferSectors_error:
pop {r0,r1,r4-r7, pc}
cf_performTransferSectors_interwork:
bx r7
.balign 4
.pool
.global cf_performTransferSectors_reg_sector_count
cf_performTransferSectors_reg_sector_count:
.word 0
.global cf_performTransferSectors_waitCardAvailableForCommands
cf_performTransferSectors_waitCardAvailableForCommands:
.word 0
.global cf_performTransferSectors_waitNextBlockReady
cf_performTransferSectors_waitNextBlockReady:
.word 0
.section "cf_read_write_functions", "ax"
.global cf_performTransfer_unlock_label
.global cf_performTransfer_lock_label
@ r2 srcAddr
@ r3 dstAddr
@ r4 command
@ top of stack startSector
@ below it numSectors
@ cf_performTransfer(dstAddr, srcAddr, command, startSector, numSectors)
cf_performTransfer:
@ loads EXMEMCNT register address
ldr r6, =0x04000200
@ waitstate 4,2 and arm9 slot2 access
@ r6 + 4 is EXMEMCNT, use lower 8 bits as 0
strb r6, [r6, #4]
ldr r7, cf_performTransfer_lockUnlockCard
movs r0, #0
@ if the cart requires no lock/unlock sequence, this is replaced with a nop
cf_performTransfer_unlock_label:
@ calls lockUnlockCard
bl cf_performTransfer_interwork
@ r2,r3,r4 hold variables not to be touched
ldr r7, cf_performTransfer_performTransferSectors
@ r1 holds startSector
@ r5 holds remainingSectors
pop {r1,r5}
movs r0, #0xFF
readNextSectorBlock:
subs r5, r0
ble lastRead
@ calls performTransferSectors
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
bl cf_performTransfer_interwork
beq error
@ increment sector
adds r1, r0
b readNextSectorBlock
lastRead:
adds r0, r5
@ calls performTransferSectors
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
bl cf_performTransfer_interwork
error:
ldr r7, cf_performTransfer_lockUnlockCard
movs r0, #1
@ if the cart requires no lock/unlock sequence, this is replaced with a nop
cf_performTransfer_lock_label:
@ calls lockUnlockCard
bl cf_performTransfer_interwork
@ waitstate 4,2 and arm7 slot2 access
movs r2, #0x80
@ r6 + 4 is EXMEMCNT
strb r2, [r6, #4]
pop {r4-r7, pc}
cf_performTransfer_interwork:
bx r7
@ cf_readSectors(u32 sector, void* buffer, u32 numSectors)
.type cf_readSectors, %function
.global cf_readSectors
cf_readSectors:
push {r0,r2,r4-r7, lr}
movs r4, CF_CMD_READ
movs r3, r1
ldr r2, cf_performTransfer_reg_data
b cf_performTransfer
@ cf_writeSectors(u32 sector, void* buffer, u32 numSectors)
.type cf_writeSectors, %function
.global cf_writeSectors
cf_writeSectors:
push {r0,r2,r4-r7, lr}
movs r4, CF_CMD_WRITE
ldr r3, cf_performTransfer_reg_data
movs r2, r1
b cf_performTransfer
.balign 4
.pool
.global cf_performTransfer_reg_data
cf_performTransfer_reg_data:
.word 0
.global cf_performTransfer_performTransferSectors
cf_performTransfer_performTransferSectors:
.word 0
.global cf_performTransfer_lockUnlockCard
cf_performTransfer_lockUnlockCard:
.word 0

View File

@@ -0,0 +1,15 @@
#pragma once
#include "common.h"
struct cf_registers_t
{
vu16* data;
vu16* altStatus;
vu16* command;
vu16* error;
vu16* sectorCount;
vu16* lba1;
vu16* lba2;
vu16* lba3;
vu16* lba4;
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include "common.h"
#include "sections.h"
#include "../LoaderPlatform.h"
#include "CompactFlashRegisters.h"
DEFINE_SECTION_SYMBOLS(cf_wait_functions);
extern "C" bool cf_waitCardAvailableForCommands();
extern "C" bool cf_waitNextBlockReady();
extern vu16* cf_waitFunctions_reg_cmd;
class CompactFlashStatusFunctionsPatchCode : public PatchCode
{
public:
CompactFlashStatusFunctionsPatchCode(PatchHeap& patchHeap, const cf_registers_t& registers)
: PatchCode(SECTION_START(cf_wait_functions), SECTION_SIZE(cf_wait_functions), patchHeap)
{
cf_waitFunctions_reg_cmd = registers.command;
}
const void* GetWaitCardAvailableForCommandsFunction() const
{
return GetAddressAtTarget((void*)cf_waitCardAvailableForCommands);
}
const void* GetWaitNextBlockReadyFunction() const
{
return GetAddressAtTarget((void*)cf_waitNextBlockReady);
}
};

View File

@@ -0,0 +1,77 @@
.cpu arm7tdmi
.syntax unified
.thumb
.section "cf_wait_functions", "ax"
.equ CF_STS_DRQ, 0x08
.equ CF_STS_DSC, 0x10
.equ CF_STS_READY, 0x40
.equ CF_STS_INSERTED, 0x50
.equ CF_STS_BUSY, 0x80
.equ CF_CARD_TIMEOUT, 10000000
@ Waits until the card is ready to receive commands
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
@ bool cf_waitCardAvailableForCommands()
.global cf_waitCardAvailableForCommands
.type cf_waitCardAvailableForCommands, %function
cf_waitCardAvailableForCommands:
push {r0-r4, lr}
@ wait for card to finish previous commands
ldr r1, =CF_CARD_TIMEOUT
ldr r2, cf_waitFunctions_reg_cmd
movs r4, r1
still_busy:
ldrh r0, [r2]
@ r0 & CF_STS_BUSY == 0
lsls r0, #25
bcc no_longer_busy
subs r1, #1
bne still_busy
no_longer_busy:
@ wait for card to be ready for commands
movs r3, (CF_STS_READY | CF_STS_DSC)
1:
ldrh r1, [r2]
tst r1, r3
@ timeout expired
bne ready
subs r4, #1
@ not ready
bne 1b
@ timeout expired, return 0
pop {r0-r4, pc}
@ Waits until the card is ready to write/return the next block
@ this function doesn't alter r0, but sets the zero flag on failure and clears it on success
@ bool cf_waitNextBlockReady()
.global cf_waitNextBlockReady
.type cf_waitNextBlockReady, %function
cf_waitNextBlockReady:
push {r0-r4, lr}
ldr r0, =CF_CARD_TIMEOUT
ldr r1, cf_waitFunctions_reg_cmd
1:
ldrb r2, [r1]
cmp r2, (CF_STS_READY | CF_STS_DSC | CF_STS_DRQ)
@ timeout expired
beq ready
subs r0, #1
@ not ready
bne 1b
@ timeout expired, return 0
pop {r0-r4, pc}
ready:
movs r0, #1
pop {r0-r4, pc}
.balign 4
.global cf_waitFunctions_reg_cmd
cf_waitFunctions_reg_cmd:
.word 0

View File

@@ -0,0 +1,13 @@
#pragma once
/// @brief Interface for patch code implementing Compact Flash carts lock/unlock routines
class ICompactFlashLockUnlockPatchCode
{
protected:
ICompactFlashLockUnlockPatchCode() { }
public:
/// @brief Gets a pointer to the SD write function in the patch code.
/// @return The pointer to the SD write function.
virtual const void* GetLockUnlockFunction() const = 0;
};