mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-10-01 21:36:53 -05:00
Merge branch 'joybus' into v7.0.0
This commit is contained in:
39
README.md
39
README.md
@@ -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).
|
||||
|
||||

|
||||
|
||||
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
BIN
docs/img/link-cube.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
284
examples/LinkCube_demo/Makefile
Normal file
284
examples/LinkCube_demo/Makefile
Normal 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
|
||||
120
examples/LinkCube_demo/src/main.cpp
Normal file
120
examples/LinkCube_demo/src/main.cpp
Normal 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;
|
||||
}
|
||||
@@ -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
274
lib/LinkCube.hpp
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user