Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions arm7/source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,39 @@
#include <libtwl/ipc/ipcSync.h>
#include <libtwl/ipc/ipcFifoSystem.h>
#include <libtwl/gfx/gfxStatus.h>
#include "ipcChannels.h"
#include "picoLoaderBootstrap.h"

// Key X/Y Input. Only the arm7 can read this register, so the arm9 asks for it
// over IPC when it needs to know whether X or Y was held at boot. Active low,
// like REG_KEYINPUT. The remaining bits (debug button, pen down, hinge) are
// deliberately not reported.
#define REG_EXTKEYIN (*(vu16*)0x04000136)
#define EXTKEYIN_KEY_X (1 << 0)
#define EXTKEYIN_KEY_Y (1 << 1)

static rtos_event_t sVBlankEvent;

static void vblankIrq(u32 irqMask)
{
rtos_signalEvent(&sVBlankEvent);
}

/// @brief Answers the arm9's request for the X/Y buttons.
///
/// Runs in interrupt context, so the reply is sent with the non-blocking send:
/// a full fifo drops the answer rather than stalling here, and the arm9 falls
/// back to treating X and Y as not held.
static void extKeysHandler(u32 channel, u32 data, void* arg)
{
(void)channel;
(void)data;
(void)arg;

const u32 heldKeys = (~REG_EXTKEYIN) & (EXTKEYIN_KEY_X | EXTKEYIN_KEY_Y);
ipc_trySendFifoMessage(IPC_CHANNEL_EXT_KEYS, heldKeys);
}

int main()
{
rtos_initIrq();
Expand All @@ -22,6 +46,10 @@ int main()

pload_init();

// Registered before the sync bits go up, so the arm9 can never ask before
// there is something here to answer.
ipc_setChannelHandler(IPC_CHANNEL_EXT_KEYS, extKeysHandler, nullptr);

rtos_createEvent(&sVBlankEvent);
rtos_setIrqFunc(RTOS_IRQ_VBLANK, vblankIrq);
rtos_enableIrqMask(RTOS_IRQ_VBLANK);
Expand Down
224 changes: 224 additions & 0 deletions arm9/source/bootConfig.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#include "bootConfig.h"
#include <string.h>

// Parser for /_pico/boot.cfg. Deliberately free of NDS headers, FatFs and
// hardware access: the caller reads the file and validates the target, so this
// stays a pure function and runs unchanged in the host tests.

namespace
{
struct KeyName
{
const char* name;
uint32_t mask;
};

// Names are uppercase; input is uppercased before comparing.
const KeyName kKeyNames[] =
{
{ "A", BOOT_KEY_A },
{ "B", BOOT_KEY_B },
{ "SELECT", BOOT_KEY_SELECT },
{ "START", BOOT_KEY_START },
{ "RIGHT", BOOT_KEY_RIGHT },
{ "LEFT", BOOT_KEY_LEFT },
{ "UP", BOOT_KEY_UP },
{ "DOWN", BOOT_KEY_DOWN },
{ "R", BOOT_KEY_R },
{ "L", BOOT_KEY_L },
{ "X", BOOT_KEY_X },
{ "Y", BOOT_KEY_Y },
};

const char kDefaultName[] = "DEFAULT";

bool isBlank(char c)
{
return c == ' ' || c == '\t';
}

char toUpperAscii(char c)
{
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - 'a' + 'A') : c;
}

/// @brief Case-insensitive compare of [begin, end) against a NUL terminated
/// uppercase name.
bool tokenEquals(const char* begin, const char* end, const char* name)
{
while (begin != end)
{
if (*name == '\0' || toUpperAscii(*begin) != *name)
{
return false;
}
begin++;
name++;
}
return *name == '\0';
}

/// @brief Copies a validated path out of the line, rejecting anything that
/// cannot be handed to Pico Loader as-is.
bool copyPath(const char* text, size_t begin, size_t end, char* outPath, size_t outSize)
{
const size_t length = end - begin;

if (length == 0 || length > BOOT_CONFIG_MAX_PATH || length + 1 > outSize)
{
return false;
}

// Paths are absolute from the card root; the "fat:" prefix is added by
// the caller, so an entry must not carry one of its own.
if (text[begin] != '/')
{
return false;
}

for (size_t i = begin; i < end; i++)
{
const unsigned char c = static_cast<unsigned char>(text[i]);
if (c < 0x20 || c == 0x7F)
{
return false;
}
}

memcpy(outPath, text + begin, length);
outPath[length] = '\0';
return true;
}

enum class MatchMode
{
Keys,
Default
};

bool findEntry(const char* text, size_t length, uint32_t heldKeys, MatchMode mode,
char* outPath, size_t outSize)
{
if (text == nullptr || outPath == nullptr || outSize == 0)
{
return false;
}

size_t pos = 0;
while (pos < length)
{
size_t lineEnd = pos;
while (lineEnd < length && text[lineEnd] != '\n')
{
lineEnd++;
}

const size_t nextLine = (lineEnd < length) ? lineEnd + 1 : length;

// Trim the CR of a CRLF pair, then leading blanks.
size_t end = lineEnd;
if (end > pos && text[end - 1] == '\r')
{
end--;
}

size_t begin = pos;
while (begin < end && isBlank(text[begin]))
{
begin++;
}

pos = nextLine;

// Blank line, or a comment. '#' only starts a comment at the start
// of a line: it is a legal character inside a file name.
if (begin >= end || text[begin] == '#')
{
continue;
}

// Only the first '=' is structural, so a path may contain more.
size_t separator = begin;
while (separator < end && text[separator] != '=')
{
separator++;
}
if (separator >= end)
{
continue;
}

size_t keyEnd = separator;
while (keyEnd > begin && isBlank(text[keyEnd - 1]))
{
keyEnd--;
}
if (keyEnd == begin)
{
continue;
}

if (mode == MatchMode::Default)
{
if (!tokenEquals(text + begin, text + keyEnd, kDefaultName))
{
continue;
}
}
else
{
uint32_t mask = 0;
for (size_t i = 0; i < sizeof(kKeyNames) / sizeof(kKeyNames[0]); i++)
{
if (tokenEquals(text + begin, text + keyEnd, kKeyNames[i].name))
{
mask = kKeyNames[i].mask;
break;
}
}

// Unknown key name, or that key is not held. Other keys may be
// held at the same time without stopping the match.
if (mask == 0 || (heldKeys & mask) != mask)
{
continue;
}
}

size_t valueBegin = separator + 1;
while (valueBegin < end && isBlank(text[valueBegin]))
{
valueBegin++;
}

size_t valueEnd = end;
while (valueEnd > valueBegin && isBlank(text[valueEnd - 1]))
{
valueEnd--;
}

if (copyPath(text, valueBegin, valueEnd, outPath, outSize))
{
return true;
}

// An entry that matched but carries an unusable path is skipped
// like any other malformed line, leaving later entries and the
// caller's fallback chain to decide.
}

return false;
}
}

bool bootConfigFindForKeys(const char* text, size_t length, uint32_t heldKeys,
char* outPath, size_t outSize)
{
return findEntry(text, length, heldKeys, MatchMode::Keys, outPath, outSize);
}

bool bootConfigFindDefault(const char* text, size_t length,
char* outPath, size_t outSize)
{
return findEntry(text, length, 0, MatchMode::Default, outPath, outSize);
}
82 changes: 82 additions & 0 deletions arm9/source/bootConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#pragma once
#include <stddef.h>
#include <stdint.h>

/// @brief Path of the boot configuration file on the DSpico SD card.
#define BOOT_CONFIG_FILE_PATH "/_pico/boot.cfg"

/// @brief Largest boot configuration file that is read. Larger files are
/// rejected rather than truncated, so a half-read entry can never be parsed.
#define BOOT_CONFIG_FILE_MAX 1024

/// @brief Longest path an entry may hold.
///
/// pload_params_t::romPath is 256 bytes and every path is handed to Pico Loader
/// with a "fat:" prefix, so the prefix and the terminator leave 251 bytes.
#define BOOT_CONFIG_MAX_PATH 251

// Key masks, matching the hardware bit order of REG_KEYINPUT (GBATEK). Note
// that libtwl's own KEYINPUT_KEY_DPAD_LEFT/DPAD_RIGHT have those two bits
// swapped relative to the hardware, so these are defined here rather than
// taken from libtwl (which is an arm7 header in this tree anyway).
#define BOOT_KEY_A (1u << 0)
#define BOOT_KEY_B (1u << 1)
#define BOOT_KEY_SELECT (1u << 2)
#define BOOT_KEY_START (1u << 3)
#define BOOT_KEY_RIGHT (1u << 4)
#define BOOT_KEY_LEFT (1u << 5)
#define BOOT_KEY_UP (1u << 6)
#define BOOT_KEY_DOWN (1u << 7)
#define BOOT_KEY_R (1u << 8)
#define BOOT_KEY_L (1u << 9)

// X and Y are not in REG_KEYINPUT: they live in REG_EXTKEYIN, which only the
// arm7 can read, so the arm9 asks for them over IPC and folds the answer into
// these two bits. They continue the REG_KEYINPUT numbering purely for
// convenience -- the hardware has no bits 10 and 11.
#define BOOT_KEY_X (1u << 10)
#define BOOT_KEY_Y (1u << 11)

/// @brief All keys readable from REG_KEYINPUT on the arm9.
#define BOOT_KEY_MASK 0x3FFu

/// @brief The keys that have to come from the arm7.
#define BOOT_KEY_EXT_MASK (BOOT_KEY_X | BOOT_KEY_Y)

/// @brief Chord that bypasses the boot configuration entirely and always boots
/// the built-in default. The escape hatch for an entry that points at a file
/// which exists but is not bootable.
#define BOOT_KEY_RECOVERY (BOOT_KEY_L | BOOT_KEY_R | BOOT_KEY_SELECT)

/// @brief Looks up the entry for the keys held at boot.
///
/// Entries are `KEY=/absolute/path`, one per line, where KEY is one of A, B,
/// SELECT, START, UP, DOWN, LEFT, RIGHT, L, R, X, Y. Lines whose first
/// non-blank character is '#' are comments, blank lines are ignored, and so are
/// entries that are malformed, name an unknown key, or hold a path that is
/// relative, empty, too long for @p outSize, longer than BOOT_CONFIG_MAX_PATH,
/// or contains control characters. The first entry whose key is held wins, so
/// the order of the file is the user's priority control. Only the first '=' is
/// structural, and '#' is a legal path character anywhere but the line start.
///
/// @param text The configuration file contents. Need not be NUL terminated.
/// @param length Length of @p text in bytes.
/// @param heldKeys Keys held at boot, as BOOT_KEY_* bits (active high).
/// @param outPath Receives the path, NUL terminated, on a match.
/// @param outSize Size of @p outPath in bytes.
/// @return true when an entry matched and @p outPath was written.
bool bootConfigFindForKeys(const char* text, size_t length, uint32_t heldKeys,
char* outPath, size_t outSize);

/// @brief Looks up the `DEFAULT=` entry, used when no key entry matched or the
/// one that did pointed at a file that could not be opened.
///
/// Parsing and validation are identical to bootConfigFindForKeys().
///
/// @param text The configuration file contents. Need not be NUL terminated.
/// @param length Length of @p text in bytes.
/// @param outPath Receives the path, NUL terminated, on a match.
/// @param outSize Size of @p outPath in bytes.
/// @return true when a DEFAULT entry matched and @p outPath was written.
bool bootConfigFindDefault(const char* text, size_t length,
char* outPath, size_t outSize);
Loading