Merge branch 'joybus' into v7.0.0

This commit is contained in:
Rodrigo Alfonso
2024-08-25 04:13:34 -03:00
9 changed files with 743 additions and 27 deletions

View File

@@ -15,6 +15,7 @@ A set of Game Boy Advance (GBA) C++ libraries to interact with the Serial Port.
- [🔌](#-LinkGPIO) [LinkGPIO.hpp](lib/LinkGPIO.hpp): Use the Link Port however you want to control **any device** (like LEDs, rumble motors, and that kind of stuff)!
- [🔗](#-LinkSPI) [LinkSPI.hpp](lib/LinkSPI.hpp): Connect with a PC (like a **Raspberry Pi**) or another GBA (with a GBC Link Cable) using this mode. Transfer up to 2Mbit/s!
- [⏱️](#%EF%B8%8F-LinkUART) [LinkUART.hpp](lib/LinkUART.hpp): Easily connect to **any PC** using a USB to UART cable!
- [🟪](#-LinkCube) [LinkCube.hpp](lib/LinkCube.hpp): Exchange data with a *Wii* or a *GameCube* using the classic **Joybus** protocol!
- [📱](#-LinkMobile) [LinkMobile.hpp](lib/LinkMobile.hpp): Connect to **the internet** using the *Mobile Adapter GB*, brought back to life thanks to the [REON](https://github.com/REONTeam) project!
- [🖱️](#%EF%B8%8F-LinkPS2Mouse) [LinkPS2Mouse.hpp](lib/LinkPS2Mouse.hpp): Connect a **PS/2 mouse** to the GBA for extended controls!
- [⌨️](#%EF%B8%8F-LinkPS2Keyboard) [LinkPS2Keyboard.hpp](lib/LinkPS2Keyboard.hpp): Connect a **PS/2 keyboard** to the GBA for extended controls!
@@ -81,8 +82,8 @@ You can update these values at any time without creating a new instance:
You can also change these compile-time constants:
- `LINK_CABLE_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max **per player**). The default value is `15`, which seems fine for most games.
- This affects how much memory is allocated. With the default value, it's `390` bytes. There's a double-buffered pending queue (to avoid data races), `1` incoming queue and `1` outgoing queue.
- You can calculate the memory usage with:
- This affects how much memory is allocated. With the default value, it's around `390` bytes. There's a double-buffered pending queue (to avoid data races), `1` incoming queue and `1` outgoing queue.
- You can approximate the memory usage with:
- `(LINK_CABLE_QUEUE_SIZE * sizeof(u16) * LINK_CABLE_MAX_PLAYERS) * 3 + LINK_CABLE_QUEUE_SIZE * sizeof(u16)` <=> `LINK_CABLE_QUEUE_SIZE * 26`
## Methods
@@ -186,8 +187,8 @@ You can update these values at any time without creating a new instance:
You can also change these compile-time constants:
- `LINK_WIRELESS_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max). The default value is `30`, which seems fine for most games.
- This affects how much memory is allocated. With the default value, it's `960` bytes. There's a double-buffered incoming queue and a double-buffered outgoing queue (to avoid data races).
- You can calculate the memory usage with:
- This affects how much memory is allocated. With the default value, it's around `960` bytes. There's a double-buffered incoming queue and a double-buffered outgoing queue (to avoid data races).
- You can approximate the memory usage with:
- `LINK_WIRELESS_QUEUE_SIZE * sizeof(Message) * 4` <=> `LINK_WIRELESS_QUEUE_SIZE * 32`
- `LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH` and `LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH`: to set the biggest allowed transfer per timer tick. Transfers contain retransmission headers and multiple user messages. These values must be in the range `[6;20]` for servers and `[2;4]` for clients. The default values are `20` and `4`, but you might want to set them a bit lower to reduce CPU usage.
- `LINK_WIRELESS_PUT_ISR_IN_IWRAM`: to put critical functions in IWRAM, which can significantly improve performance due to its faster access. This is disabled by default to conserve IWRAM space, which is limited, but it's enabled in demos to showcase its performance benefits.
@@ -421,6 +422,36 @@ The GBA operates using `1` stop bit, but everything else can be configured. By d
- Green wire (`TX`) -> GBA `SI`.
- White wire (`RX`) -> GBA `SO`.
# 🟪 LinkCube
*(aka JOYBUS Mode)*
This is the GBA's implementation of JOYBUS, in which users connect the console to a *GameCube* (or *Wii* with GC ports) using an official adapter. The library can be tested using *Dolphin/mGBA* and [gba-joybus-tester](https://github.com/afska/gba-joybus-tester).
![screenshot](https://github.com/user-attachments/assets/93c11c9a-bdbf-4726-a070-895465739789)
You can change these compile-time constants:
- `LINK_CUBE_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing values the queues can store at max). The default value is `10`, which seems fine for most games.
- This affects how much memory is allocated. With the default value, it's around `120` bytes. There's a double-buffered pending queue (to avoid data races), and 1 outgoing queue.
- You can approximate the memory usage with:
- `LINK_CUBE_QUEUE_SIZE * sizeof(u32) * 3` <=> `LINK_CUBE_QUEUE_SIZE * 12`
## Methods
Name | Return type | Description
--- | --- | ---
`isActive()` | **bool** | Returns whether the library is active or not.
`activate()` | - | Activates the library.
`deactivate()` | - | Deactivates the library.
`wait()` | **bool** | Waits for data. Returns `true` on success, or `false` on JOYBUS reset.
`wait(cancel)` | **bool** | Like `wait()` but accepts a `cancel()` function. The library will continuously invoke it, and abort the wait if it returns `true`.
`canRead()` | **bool** | Returns `true` if there are pending received values to read.
`read()` | **u32** | Dequeues and returns the next received value. If there's no received data, a `0` will be returned.
`peek()` | **u32** | Returns the next received value without dequeuing it. If there's no received data, a `0` will be returned.
`send(data)` | - | Sends 32-bit `data`. If the other end asks for data at the same time you call this method, a `0x00000000` will be sent.
`pendingCount()` | **u32** | Returns the number of pending outgoing transfers.
`didReset([clear])` | **bool** | Returns whether a JOYBUS reset was requested or not. After this call, the reset flag is cleared if `clear` is `true` (default behavior).
# 📱 LinkMobile
*(aka Mobile Adapter GB)*

BIN
docs/img/link-cube.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -0,0 +1,284 @@
#
# Template tonc makefile
#
# Yoinked mostly from DKP's template
#
# === SETUP ===========================================================
# --- No implicit rules ---
.SUFFIXES:
# --- Paths ---
export TONCLIB := ${DEVKITPRO}/libtonc
# === TONC RULES ======================================================
#
# Yes, this is almost, but not quite, completely like to
# DKP's base_rules and gba_rules
#
export PATH := $(DEVKITARM)/bin:$(PATH)
# --- Executable names ---
PREFIX ?= arm-none-eabi-
export CC := $(PREFIX)gcc
export CXX := $(PREFIX)g++
export AS := $(PREFIX)as
export AR := $(PREFIX)ar
export NM := $(PREFIX)nm
export OBJCOPY := $(PREFIX)objcopy
# LD defined in Makefile
# === LINK / TRANSLATE ================================================
%.gba : %.elf
@$(OBJCOPY) -O binary $< $@
@echo built ... $(notdir $@)
@gbafix $@ -t$(TITLE)
#----------------------------------------------------------------------
%.mb.elf :
@echo Linking multiboot
$(LD) -specs=gba_mb.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@
$(NM) -Sn $@ > $(basename $(notdir $@)).map
#----------------------------------------------------------------------
%.elf :
@echo Linking cartridge
$(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@
$(NM) -Sn $@ > $(basename $(notdir $@)).map
#----------------------------------------------------------------------
%.a :
@echo $(notdir $@)
@rm -f $@
$(AR) -crs $@ $^
# === OBJECTIFY =======================================================
%.iwram.o : %.iwram.cpp
@echo $(notdir $<)
$(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@
#----------------------------------------------------------------------
%.iwram.o : %.iwram.c
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(IARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.cpp
@echo $(notdir $<)
$(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(RARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.c
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(RARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.s
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.S
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
#----------------------------------------------------------------------
# canned command sequence for binary data
#----------------------------------------------------------------------
define bin2o
bin2s $< | $(AS) -o $(@)
echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(<F) | tr . _)`.h
echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(<F) | tr . _)`.h
echo "extern const u32" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(<F) | tr . _)`.h
endef
# =====================================================================
# --- Main path ---
export PATH := $(DEVKITARM)/bin:$(PATH)
# === PROJECT DETAILS =================================================
# PROJ : Base project name
# TITLE : Title for ROM header (12 characters)
# LIBS : Libraries to use, formatted as list for linker flags
# BUILD : Directory for build process temporaries. Should NOT be empty!
# SRCDIRS : List of source file directories
# DATADIRS : List of data file directories
# INCDIRS : List of header file directories
# LIBDIRS : List of library directories
# General note: use `.' for the current dir, don't leave the lists empty.
export PROJ ?= $(notdir $(CURDIR))
TITLE := $(PROJ)
LIBS := -ltonc -lugba
BUILD := build
SRCDIRS := src ../_lib ../../lib
DATADIRS := data
INCDIRS := src
LIBDIRS := $(TONCLIB) $(PWD)/../_lib/libugba
# --- switches ---
bMB := 0 # Multiboot build
bTEMPS := 0 # Save gcc temporaries (.i and .s files)
bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet)
# === BUILD FLAGS =====================================================
# This is probably where you can stop editing
# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck
# up things (gcse seems fond of building masks inside a loop instead of
# outside them for example). Removing them sometimes helps
# --- Architecture ---
ARCH := -mthumb-interwork -mthumb
RARCH := -mthumb-interwork -mthumb
IARCH := -mthumb-interwork -marm -mlong-calls
# --- Main flags ---
CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast
CFLAGS += -Wall
CFLAGS += $(INCLUDE)
CFLAGS += -ffast-math -fno-strict-aliasing
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17
ASFLAGS := $(ARCH) $(INCLUDE)
LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map
# --- switched additions ----------------------------------------------
# --- Multiboot ? ---
ifeq ($(strip $(bMB)), 1)
TARGET := $(PROJ).mb
else
TARGET := $(PROJ)
endif
# --- Save temporary files ? ---
ifeq ($(strip $(bTEMPS)), 1)
CFLAGS += -save-temps
CXXFLAGS += -save-temps
endif
# --- Debug info ? ---
ifeq ($(strip $(bDEBUG)), 1)
CFLAGS += -DDEBUG -g
CXXFLAGS += -DDEBUG -g
ASFLAGS += -DDEBUG -g
LDFLAGS += -g
else
CFLAGS += -DNDEBUG
CXXFLAGS += -DNDEBUG
ASFLAGS += -DNDEBUG
endif
# === BUILD PROC ======================================================
ifneq ($(BUILD),$(notdir $(CURDIR)))
# Still in main dir:
# * Define/export some extra variables
# * Invoke this file again from the build dir
# PONDER: what happens if BUILD == "" ?
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := \
$(foreach dir, $(SRCDIRS) , $(CURDIR)/$(dir)) \
$(foreach dir, $(DATADIRS), $(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
# --- List source and data files ---
CFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir, $(DATADIRS), $(notdir $(wildcard $(dir)/*.*)))
# --- Set linker depending on C++ file existence ---
ifeq ($(strip $(CPPFILES)),)
export LD := $(CC)
else
export LD := $(CXX)
endif
# --- Define object file list ---
export OFILES := $(addsuffix .o, $(BINFILES)) \
$(CFILES:.c=.o) $(CPPFILES:.cpp=.o) \
$(SFILES:.s=.o)
# --- Create include and library search paths ---
export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
# --- Create BUILD if necessary, and run this makefile from there ---
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@make --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
arm-none-eabi-nm -Sn $(OUTPUT).elf > $(BUILD)/$(TARGET).map
all : $(BUILD)
clean:
@echo clean ...
@rm -rf $(BUILD) $(TARGET).elf $(TARGET).gba $(TARGET).sav
else # If we're here, we should be in the BUILD dir
DEPENDS := $(OFILES:.o=.d)
# --- Main targets ----
$(OUTPUT).gba : $(OUTPUT).elf
$(OUTPUT).elf : $(OFILES)
-include $(DEPENDS)
endif # End BUILD switch
# --- More targets ----------------------------------------------------
.PHONY: clean rebuild start
rebuild: clean $(BUILD)
start:
start "$(TARGET).gba"
restart: rebuild start
# EOF

View File

@@ -0,0 +1,120 @@
// (0) Include the header
#include "../../../lib/LinkCube.hpp"
#include <tonc.h>
#include <string>
#include "../../_lib/interrupt.h"
void log(std::string text);
void wait(u32 verticalLines);
bool didPress(unsigned short key, bool& pressed);
inline void VBLANK() {}
bool a = false, b = false, l = false;
// (1) Create a LinkCube instance
LinkCube* linkCube = new LinkCube();
void init() {
REG_DISPCNT = DCNT_MODE0 | DCNT_BG0;
tte_init_se_default(0, BG_CBB(0) | BG_SBB(31));
// (2) Add the interrupt service routines
interrupt_init();
interrupt_set_handler(INTR_VBLANK, VBLANK);
interrupt_enable(INTR_VBLANK);
interrupt_set_handler(INTR_SERIAL, LINK_CUBE_ISR_SERIAL);
interrupt_enable(INTR_SERIAL);
}
int main() {
init();
// (3) Initialize the library
linkCube->activate();
u32 counter = 0;
std::string received = "";
bool reset = false;
u32 vblanks = 0;
while (true) {
// Title
std::string output =
"LinkCube_demo (v7.0.0)" + std::string(reset ? " !RESET!" : "") +
"\n\nPress A to send\nPress B to clear\n (L = "
"+1024)\n\nLast sent: " +
std::to_string(counter) +
"\n(pending = " + std::to_string(linkCube->pendingCount()) +
")\n\nReceived:\n" + received;
// (4) Send 32-bit values
if (didPress(KEY_A, a)) {
counter++;
linkCube->send(counter);
}
// +1024
if (didPress(KEY_L, l)) {
counter += 1024;
linkCube->send(counter);
}
// (5) Read 32-bit values
while (linkCube->canRead()) {
received += std::to_string(linkCube->read()) + ", ";
}
// Clear
if (didPress(KEY_B, b))
received = "";
// Reset warning
if (linkCube->didReset()) {
counter = 0;
reset = true;
vblanks = 0;
}
if (reset) {
vblanks++;
if (vblanks > 60)
reset = false;
}
// Print
VBlankIntrWait();
log(output);
}
return 0;
}
void log(std::string text) {
tte_erase_screen();
tte_write("#{P:0,0}");
tte_write(text.c_str());
}
void wait(u32 verticalLines) {
u32 count = 0;
u32 vCount = REG_VCOUNT;
while (count < verticalLines) {
if (REG_VCOUNT != vCount) {
count++;
vCount = REG_VCOUNT;
}
};
}
bool didPress(u16 key, bool& pressed) {
u16 keys = ~REG_KEYS & KEY_ANY;
bool isPressedNow = false;
if ((keys & key) && !pressed) {
pressed = true;
isPressedNow = true;
}
if (pressed && !(keys & key))
pressed = false;
return isPressedNow;
}

View File

@@ -45,13 +45,13 @@
#include "_link_common.hpp"
/**
* @brief Buffer size (how many incoming and outgoing messages
* the queues can store at max **per player**). The default value is `15`, which
* seems fine for most games.
* @brief Buffer size (how many incoming and outgoing messages the queues can
* store at max **per player**). The default value is `15`, which seems fine for
* most games.
* \warning This affects how much memory is allocated. With the default value,
* it's `390` bytes. There's a double-buffered pending queue (to avoid data
* races), 1 incoming queue and 1 outgoing queue.
* \warning You can calculate the usage with `LINK_CABLE_QUEUE_SIZE * 26`.
* it's around `390` bytes. There's a double-buffered pending queue (to avoid
* data races), 1 incoming queue and 1 outgoing queue. \warning You can
* approximate the usage with `LINK_CABLE_QUEUE_SIZE * 26`.
*/
#define LINK_CABLE_QUEUE_SIZE 15

274
lib/LinkCube.hpp Normal file
View File

@@ -0,0 +1,274 @@
#ifndef LINK_CUBE_H
#define LINK_CUBE_H
// --------------------------------------------------------------------------
// A JOYBUS handler for the Link Port.
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
// LinkCube* linkCube = new LinkCube();
// - 2) Add the required interrupt service routines: (*)
// irq_init(NULL);
// irq_add(II_SERIAL, LINK_CUBE_ISR_SERIAL);
// - 3) Initialize the library with:
// linkCube->activate();
// - 4) Send 32-bit values:
// linkCube->send(0x12345678);
// // (now linkCube->pendingCount() will be 1 until it's actually sent)
// - 5) Read 32-bit values:
// if (linkCube->canRead()) {
// u32 value = linkCube->read();
// // ...
// }
// --------------------------------------------------------------------------
// (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug.
// That causes packet loss. You REALLY want to use libugba's instead.
// (see examples)
// --------------------------------------------------------------------------
#include "_link_common.hpp"
/**
* @brief Buffer size (how many incoming and outgoing values the queues can
* store at max). The default value is `10`, which seems fine for most games.
* \warning This affects how much memory is allocated. With the default value,
* it's around `120` bytes. There's a double-buffered pending queue (to avoid
* data races), and 1 outgoing queue.
* \warning You can approximate the usage with `LINK_CUBE_QUEUE_SIZE * 12`.
*/
#define LINK_CUBE_QUEUE_SIZE 10
static volatile char LINK_CUBE_VERSION[] = "LinkCube/v7.0.0";
#define LINK_CUBE_BARRIER asm volatile("" ::: "memory")
/**
* @brief A JOYBUS handler for the Link Port.
*/
class LinkCube {
private:
using u32 = unsigned int;
using u16 = unsigned short;
using u8 = unsigned char;
using U32Queue = Link::Queue<u32, LINK_CUBE_QUEUE_SIZE>;
static constexpr int BIT_CMD_RESET = 0;
static constexpr int BIT_CMD_RECEIVE = 1;
static constexpr int BIT_CMD_SEND = 2;
static constexpr int BIT_IRQ = 6;
static constexpr int BIT_JOYBUS_HIGH = 14;
static constexpr int BIT_GENERAL_PURPOSE_LOW = 14;
static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15;
public:
/**
* @brief Returns whether the library is active or not.
*/
[[nodiscard]] bool isActive() { return isEnabled; }
/**
* @brief Activates the library.
*/
void activate() {
LINK_CUBE_BARRIER;
isEnabled = false;
LINK_CUBE_BARRIER;
resetState();
stop();
LINK_CUBE_BARRIER;
isEnabled = true;
LINK_CUBE_BARRIER;
start();
}
/**
* @brief Deactivates the library.
*/
void deactivate() {
isEnabled = false;
resetState();
stop();
}
/**
* @brief Waits for data. Returns `true` on success, or `false` on
* JOYBUS reset.
*/
bool wait() {
return wait([]() { return false; });
}
/**
* @brief Waits for data. Returns `true` on success, or `false` on
* JOYBUS reset or cancellation.
* @param cancel A function that will be continuously invoked. If it returns
* `true`, the wait be aborted.
*/
template <typename F>
bool wait(F cancel) {
resetFlag = false;
while (!resetFlag && !canRead() && !cancel())
Link::_IntrWait(1, Link::_IRQ_SERIAL);
return canRead();
}
/**
* @brief Returns `true` if there are pending received values to read.
*/
[[nodiscard]] bool canRead() { return !incomingQueue.isEmpty(); }
/**
* @brief Dequeues and returns the next received value.
* \warning If there's no received data, a `0` will be returned.
*/
u32 read() { return incomingQueue.syncPop(); }
/**
* @brief Returns the next received value without dequeuing it.
* \warning If there's no received data, a `0` will be returned.
*/
[[nodiscard]] u32 peek() { return incomingQueue.peek(); }
/**
* @brief Sends 32-bit `data`.
* @param data The value to be sent.
* \warning If the other end asks for data at the same time you call this
* method, a `0x00000000` will be sent.
*/
void send(u32 data) { outgoingQueue.syncPush(data); }
/**
* @brief Returns the number of pending outgoing transfers.
*/
[[nodiscard]] u32 pendingCount() { return outgoingQueue.size(); }
/**
* @brief Returns whether a JOYBUS reset was requested or not. After this
* call, the reset flag is cleared if `clear` is `true` (default behavior).
* @param clear Whether it should clear the reset flag or not.
*/
bool didReset(bool clear = true) {
bool reset = resetFlag;
if (clear)
resetFlag = false;
return reset;
}
/**
* @brief This method is called by the SERIAL interrupt handler.
* \warning This is internal API!
*/
void _onSerial() {
if (!isEnabled)
return;
if (isBitHigh(BIT_CMD_RESET)) {
resetState();
resetFlag = true;
setBitHigh(BIT_CMD_RESET);
}
if (isBitHigh(BIT_CMD_RECEIVE)) {
newIncomingQueue.push(getData());
setBitHigh(BIT_CMD_RECEIVE);
}
if (isBitHigh(BIT_CMD_SEND)) {
setPendingData();
setBitHigh(BIT_CMD_SEND);
}
copyState();
}
private:
U32Queue newIncomingQueue;
U32Queue incomingQueue;
U32Queue outgoingQueue;
volatile bool resetFlag = false;
volatile bool needsClear = false;
volatile bool isEnabled = false;
void copyState() {
if (incomingQueue.isReading())
return;
if (needsClear) {
incomingQueue.clear();
needsClear = false;
}
while (!newIncomingQueue.isEmpty())
incomingQueue.push(newIncomingQueue.pop());
}
void resetState() {
needsClear = false;
newIncomingQueue.clear();
if (incomingQueue.isReading())
needsClear = true;
else
incomingQueue.clear();
outgoingQueue.syncClear();
resetFlag = false;
}
void setPendingData() {
setData(outgoingQueue.isWriting() ? 0 : outgoingQueue.pop());
}
void setData(u32 data) {
Link::_REG_JOY_TRANS_H = msB32(data);
Link::_REG_JOY_TRANS_L = lsB32(data);
}
u32 getData() {
return buildU32(Link::_REG_JOY_RECV_H, Link::_REG_JOY_RECV_L);
}
void stop() {
setInterruptsOff();
setGeneralPurposeMode();
}
void start() {
setJoybusMode();
setInterruptsOn();
}
void setJoybusMode() {
Link::_REG_RCNT = Link::_REG_RCNT | (1 << BIT_JOYBUS_HIGH) |
(1 << BIT_GENERAL_PURPOSE_HIGH);
}
void setGeneralPurposeMode() {
Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) |
(1 << BIT_GENERAL_PURPOSE_HIGH);
}
void setInterruptsOn() { setBitHigh(BIT_IRQ); }
void setInterruptsOff() { setBitLow(BIT_IRQ); }
u32 buildU32(u16 msB, u16 lsB) { return (msB << 16) | lsB; }
u16 msB32(u32 value) { return value >> 16; }
u16 lsB32(u32 value) { return value & 0xffff; }
bool isBitHigh(u8 bit) { return (Link::_REG_JOYCNT >> bit) & 1; }
void setBitHigh(u8 bit) { Link::_REG_JOYCNT |= 1 << bit; }
void setBitLow(u8 bit) { Link::_REG_JOYCNT &= ~(1 << bit); }
};
extern LinkCube* linkCube;
/**
* @brief SERIAL interrupt handler.
*/
inline void LINK_CUBE_ISR_SERIAL() {
linkCube->_onSerial();
}
#endif // LINK_CUBE_H

View File

@@ -256,19 +256,7 @@ class LinkUART {
/**
* @brief Reads a byte. Returns 0 if nothing is found.
*/
u8 read() {
LINK_UART_BARRIER;
incomingQueue.startReading();
LINK_UART_BARRIER;
u8 data = incomingQueue.pop();
LINK_UART_BARRIER;
incomingQueue.stopReading();
LINK_UART_BARRIER;
return data;
}
u8 read() { return incomingQueue.syncPop(); }
/**
* @brief Sends a `data` byte.

View File

@@ -67,9 +67,9 @@
* @brief Buffer size (how many incoming and outgoing messages the queues can
* store at max). The default value is `30`, which seems fine for most games.
* \warning This affects how much memory is allocated. With the default value,
* it's `960` bytes. There's a double-buffered incoming queue and a
* it's around `960` bytes. There's a double-buffered incoming queue and a
* double-buffered outgoing queue (to avoid data races).
* \warning You can calculate the usage with `LINK_WIRELESS_QUEUE_SIZE * 32`.
* \warning You can approximate the usage with `LINK_WIRELESS_QUEUE_SIZE * 32`.
*/
#define LINK_WIRELESS_QUEUE_SIZE 30

View File

@@ -76,6 +76,12 @@ inline vu32& _REG_SIODATA32 = *reinterpret_cast<vu32*>(_REG_BASE + 0x0120);
inline vu16& _REG_SIODATA8 = *reinterpret_cast<vu16*>(_REG_BASE + 0x012A);
inline vu16& _REG_SIOMLT_SEND = *reinterpret_cast<vu16*>(_REG_BASE + 0x012A);
inline vu16* const _REG_SIOMULTI = reinterpret_cast<vu16*>(_REG_BASE + 0x0120);
inline vu16& _REG_JOYCNT = *reinterpret_cast<vu16*>(_REG_BASE + 0x0140);
inline vu16& _REG_JOY_RECV_L = *reinterpret_cast<vu16*>(_REG_BASE + 0x0150);
inline vu16& _REG_JOY_RECV_H = *reinterpret_cast<vu16*>(_REG_BASE + 0x0152);
inline vu16& _REG_JOY_TRANS_L = *reinterpret_cast<vu16*>(_REG_BASE + 0x0154);
inline vu16& _REG_JOY_TRANS_H = *reinterpret_cast<vu16*>(_REG_BASE + 0x0156);
inline vu16& _REG_JOYSTAT = *reinterpret_cast<vu16*>(_REG_BASE + 0x0158);
inline vu16& _REG_VCOUNT = *reinterpret_cast<vu16*>(_REG_BASE + 0x0006);
inline vu16& _REG_KEYS = *reinterpret_cast<vu16*>(_REG_BASE + 0x0130);
inline vu16& _REG_TM1CNT_L = *reinterpret_cast<vu16*>(_REG_BASE + 0x0104);
@@ -213,6 +219,19 @@ class Queue {
}
}
T syncPop() {
_isReading = true;
asm volatile("" ::: "memory");
auto value = pop();
asm volatile("" ::: "memory");
_isReading = false;
asm volatile("" ::: "memory");
return value;
}
void syncClear() {
if (_isReading)
return; // (it will be cleared later anyway)
@@ -224,8 +243,8 @@ class Queue {
}
u32 size() { return count; }
bool isEmpty() { return size() == 0; }
bool isFull() { return size() == Size; }
bool isEmpty() { return count == 0; }
bool isFull() { return count == Size; }
bool isReading() { return _isReading; }
bool isWriting() { return _isWriting; }
bool canMutate() { return !_isReading && !_isWriting; }