1
0
mirror of https://github.com/mnzlmstr/N64toiQue synced 2024-11-22 09:12:21 -05:00

Initial commit

This commit is contained in:
mnzlmstr 2020-04-12 22:55:15 +02:00
commit 7c272f7a90
9 changed files with 519 additions and 0 deletions

206
ControllerBuffer.cpp Normal file
View File

@ -0,0 +1,206 @@
#include "ControllerBuffer.h"
#include "PinMappings.h"
#include "Debug.h"
// buffer to hold data being read from controller
bool buffer[DATA_SIZE + DATA_OFFSET];
// bit resolution and offsets
int bitOffsets[32];
int bitResolution;
/** Function to send a Command to the attached N64-Controller.
* Must be run from RAM to defy timing differences introduced from
* reading Code from ESP32's SPI Flash Chip.
*/
void IRAM_ATTR sendCommand(byte command)
{
// the current bit to write
bool bit;
// clear output buffer
memset(buffer,0,DATA_SIZE + DATA_OFFSET);
// for each bit
for (int i = 0; i < 8; i++)
{
// get value
bit = (1 << (7 - i)) & command;
// write data
LINE_WRITE_LOW;
delayMicroseconds((3 - 2 * bit));
LINE_WRITE_HIGH;
delayMicroseconds((1 + 2 * bit));
}
// console stop bit
LINE_WRITE_LOW;
delayMicroseconds(1);
LINE_WRITE_HIGH;
delayMicroseconds(2);
// read returned data as fast as possible
for(int i = 0;i < DATA_SIZE + DATA_OFFSET;i++)
{
buffer[i] = digitalRead(DATA_PIN);
}
// plot polling process from controller if unstructed to
#ifdef PLOT_CONSOLE_POLLING
for(int i = 0; i < DATA_SIZE + DATA_OFFSET;i++)
{
Serial.println(buffer[i]*2500);
}
#endif
}
/* Function to extract a controller bit from the buffer of returned data */
void getBit(bool *bit,int offset,bool *data)
{
// sanity check offset
if(offset < 0) offset = 0;
// count
short count = 0;
// get count from offset to offset + length
for(int i = offset + DATA_OFFSET;i < offset + bitResolution;i++)
{
count += *(data + i);
}
// if offset surpasses threshold set bit
*bit = false;
if(count > BIT_THRESHOLD) *bit = true;
}
/** Function to populate the controller struct if command 0x01 was sent.
* Buttons are set according to data in buffer, raw axis data is written,
* Axis Data is correctly decoded from raw axis data by taking two's complement
* and checking if value if below 'MAX_INCLINE_AXIS_X' or 'MAX_INCLINE_AXIS_Y'.
* If values surpass the maximum incline they are set to match those values.
*/
void populateControllerStruct(ControllerData *data)
{
// first byte
getBit(&(data->buttonA) , bitOffsets[0] ,&buffer[0]);
getBit(&(data->buttonB), bitOffsets[1] ,&buffer[0]);
getBit(&(data->buttonZ), bitOffsets[2] ,&buffer[0]);
getBit(&(data->buttonStart), bitOffsets[3] ,&buffer[0]);
getBit(&(data->DPadUp), bitOffsets[4] ,&buffer[0]);
getBit(&(data->DPadDown), bitOffsets[5] ,&buffer[0]);
getBit(&(data->DPadLeft), bitOffsets[6] ,&buffer[0]);
getBit(&(data->DPadRight), bitOffsets[7] ,&buffer[0]);
// second byte, first two bits are unused
getBit(&(data->buttonL), bitOffsets[10] ,&buffer[0]);
getBit(&(data->buttonR), bitOffsets[11] ,&buffer[0]);
getBit(&(data->CUp), bitOffsets[12] ,&buffer[0]);
getBit(&(data->CDown), bitOffsets[13] ,&buffer[0]);
getBit(&(data->CLeft), bitOffsets[14] ,&buffer[0]);
getBit(&(data->CRight), bitOffsets[15] ,&buffer[0]);
// third byte
getBit(&(data->xAxisRaw[0]), bitOffsets[16], &buffer[0]);
getBit(&(data->xAxisRaw[1]), bitOffsets[17], &buffer[0]);
getBit(&(data->xAxisRaw[2]), bitOffsets[18], &buffer[0]);
getBit(&(data->xAxisRaw[3]), bitOffsets[19], &buffer[0]);
getBit(&(data->xAxisRaw[4]), bitOffsets[20], &buffer[0]);
getBit(&(data->xAxisRaw[5]), bitOffsets[21], &buffer[0]);
getBit(&(data->xAxisRaw[6]), bitOffsets[22], &buffer[0]);
getBit(&(data->xAxisRaw[7]), bitOffsets[23], &buffer[0]);
// fourth byte
getBit(&(data->yAxisRaw[0]), bitOffsets[24], &buffer[0]);
getBit(&(data->yAxisRaw[1]), bitOffsets[25], &buffer[0]);
getBit(&(data->yAxisRaw[2]), bitOffsets[26], &buffer[0]);
getBit(&(data->yAxisRaw[3]), bitOffsets[27], &buffer[0]);
getBit(&(data->yAxisRaw[4]), bitOffsets[28], &buffer[0]);
getBit(&(data->yAxisRaw[5]), bitOffsets[29], &buffer[0]);
getBit(&(data->yAxisRaw[6]), bitOffsets[30], &buffer[0]);
getBit(&(data->yAxisRaw[7]), bitOffsets[31], &buffer[0]);
// sum up bits to get axis bytes
data->xAxis = 0;
data->yAxis = 0;
for(int i = 0;i < 8;i++)
{
data->xAxis += (data->xAxisRaw[i] * (0x80 >> (i)));
data->yAxis += (data->yAxisRaw[i] * (0x80 >> (i)));
// print y axis values
#ifdef PRINT_Y_AXIS_VALUES
Serial.printf("%i %i %i %i %i %i %i %i\n",data->yAxisRaw[0],data->yAxisRaw[1],data->yAxisRaw[2],data->yAxisRaw[3],data->yAxisRaw[4],data->yAxisRaw[5],data->yAxisRaw[6],data->yAxisRaw[7]);
Serial.printf("yAxis: %i \n",data->yAxis);
#endif
// print x axis values
#ifdef PRINT_X_AXIS_VALUES
Serial.printf("%i %i %i %i %i %i %i %i\n",data->xAxisRaw[0],data->xAxisRaw[1],data->xAxisRaw[2],data->xAxisRaw[3],data->xAxisRaw[4],data->xAxisRaw[5],data->xAxisRaw[6],data->xAxisRaw[7]);
Serial.printf("xAxis: %i \n",data->xAxis);
#endif
}
// decode xAxis two's complement
if(data->xAxis & 0x80)
{
data->xAxis = -1 * (0xff - data->xAxis);
}
// decode yAxis two's complement
if(data->yAxis & 0x80)
{
data->yAxis = -1 * (0xff - data->yAxis);
}
// keep x axis below maxIncline
if(data->xAxis > MAX_INCLINE_AXIS_X) data->xAxis = MAX_INCLINE_AXIS_X;
if(data->xAxis < -MAX_INCLINE_AXIS_X) data->xAxis = -MAX_INCLINE_AXIS_X;
// keep y axis below maxIncline
if(data->yAxis > MAX_INCLINE_AXIS_Y) data->yAxis = MAX_INCLINE_AXIS_Y;
if(data->yAxis < -MAX_INCLINE_AXIS_Y) data->yAxis = -MAX_INCLINE_AXIS_Y;
//Serial.printf("xaxis: %-3i yaxis: %-3i \n",data->xAxis,data->yAxis);
}
void updateOffsetsAndResolution()
{
// the current bit counter
int bitCounter = 0;
// to hold the offset of A Button's falling edge
int bitAfallingOffset = 0;
// iterate over buffer
for(int i = 0;i < DATA_SIZE + DATA_OFFSET - 1;i++)
{
// if a falling edge is detected
if(buffer[i] == true && buffer[1+i] == false)
{
// store bit's end offset
bitOffsets[bitCounter] = i+1;
// if it's the A button store offset of the falling edge
if(bitCounter == 0) bitAfallingOffset = i+1;
// if it's the B button calculate the bit Resolution
if(bitCounter == 1) bitResolution = (i+1) - bitAfallingOffset;
// increment bit counter
bitCounter++;
}
}
Serial.printf("Bit resolution is %i \n",bitResolution);
// calculate bit's beginning offsets by subtracting resolution
for(int i = 0;i < 32;i++)
{
bitOffsets[i] -= bitResolution;
Serial.printf("beginning of bit %i detected @ begin+%i \n",i+1,bitOffsets[i]);
}
}

26
ControllerBuffer.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef CONTROLLER_BUFFER_H
#define CONTROLLER_BUFFER_H
#include "ControllerData.h"
#include <Arduino.h>
#define DATA_SIZE 450 // number of sample points to poll
#define DATA_OFFSET 0 // number of samples to ignore after staring to poll
#define MAX_INCLINE_AXIS_X 60
#define MAX_INCLINE_AXIS_Y 60
#define BIT_THRESHOLD 6
extern void IRAM_ATTR sendCommand(byte command);
extern void populateControllerStruct(ControllerData *data);
/** Function to read the offsets of the individual bits so that they can be updated for reading.
* Resolution is detected by taking the distance of two falling edges of Button A and B.
* Buttons must not be pressed by the time this command is invoked !
* sendCommand(0x01) must be invoked before !
*/
extern void updateOffsetsAndResolution();
#endif

31
ControllerData.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef CONTROLLER_DATA_H
#define CONTROLLER_DATA_H
struct ControllerData
{
bool buttonA;
bool buttonB;
bool buttonZ;
bool buttonL;
bool buttonR;
bool buttonStart;
bool DPadUp;
bool DPadDown;
bool DPadLeft;
bool DPadRight;
bool CUp;
bool CDown;
bool CLeft;
bool CRight;
short xAxis;
short yAxis;
bool xAxisRaw[8];
bool yAxisRaw[8];
};
#endif

8
Debug.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef DEBUG_H
#define DEBUG_H
//#define PLOT_CONSOLE_POLLING
//#define PRINT_X_AXIS_VALUES
//#define PRINT_Y_AXIS_VALUES
#endif

135
Output.cpp Normal file
View File

@ -0,0 +1,135 @@
#include "Output.h"
#include "PinMappings.h"
#include <Arduino.h>
int encoderXpos = 0;
int encoderYpos = 0;
void setupIO()
{
// the controller data line
LINE_WRITE_HIGH;
pinMode(PIN_BUTTON_A,OUTPUT);
pinMode(PIN_BUTTON_B,OUTPUT);
pinMode(PIN_BUTTON_Z,OUTPUT);
pinMode(PIN_BUTTON_S,OUTPUT);
pinMode(PIN_BUTTON_L,OUTPUT);
pinMode(PIN_BUTTON_R,OUTPUT);
pinMode(PIN_DPAD_UP, OUTPUT);
pinMode(PIN_DPAD_DOWN, OUTPUT);
pinMode(PIN_DPAD_LEFT, OUTPUT);
pinMode(PIN_DPAD_RIGHT,OUTPUT);
pinMode(PIN_C_UP, OUTPUT);
pinMode(PIN_C_DOWN, OUTPUT);
pinMode(PIN_C_LEFT, OUTPUT);
pinMode(PIN_C_RIGHT,OUTPUT);
pinMode(PIN_A_AXIS_X, OUTPUT);
pinMode(PIN_B_AXIS_X, OUTPUT);
pinMode(PIN_A_AXIS_Y, OUTPUT);
pinMode(PIN_B_AXIS_Y, OUTPUT);
}
/** Function to simulate the 90° phaseshifted quadrature encoding used by
* the N64-Controller's joystick's linear encoders. As only change in position
* (not absolute position) is reported the change in the value between writes
* is needed for calculation. Must be run from RAM to mitigate timing differences in
* ESP32's SPI-Flash Chip Access Times.
* @param relativeMovement should be processed by calling 'mapJoystickToEncoderPos'
* with the read joystick position.
*/
void IRAM_ATTR moveAxis(int axisPinA,int axisPinB,int requestedEncoderPos,int *realEncoderPos)
{
// get difference from requested ender position to real encoder position
int difference = requestedEncoderPos - *realEncoderPos;
// store new real encoder position
*realEncoderPos = requestedEncoderPos;
// set pins used for outputting
int pinA = axisPinA;
int pinB = axisPinB;
// invert pins if moving in the other direction
if(difference > 0)
{
pinA = axisPinB;
pinB = axisPinA;
}
// get absolute movement value
difference = abs(difference);
// for each change in movement
for(int i = 0;i < difference;i++)
{
// replicate 90° phaseshifted quadrate output
digitalWrite(pinA,LOW);
delayMicroseconds(2);
digitalWrite(pinB,LOW);
delayMicroseconds(6);
digitalWrite(pinA,HIGH);
delayMicroseconds(2);
digitalWrite(pinB,HIGH);
// debounce as N64 will not detect change correctly otherwise
delayMicroseconds(40);
}
}
/** Function to map Joystick position to relative Encoder Position.
* According to Nintendo the maximum value for any axis to be assumed should
* be +- 60. The encoder starts acting strange after ~ 30 turns in a given
* direction. Also passing on ControllerData struct so that user specified
* modifiers can be used to specify alternate mapping, eg. for
* easier ESS-Position in Ocarina of Time (see my example)
*/
int mapJoystickToEncoderPos(int joyval,ControllerData *data)
{
// ESS-Modifier for Ocarina of Time, hold A+B to stay in ESS-Position
if(data->buttonA && data->buttonB) return -2;
// deadzone handling
if(abs(joyval) < 3) return 0;
// normally return half ???
return (int)(joyval * .5f);
}
void outputToiQue(ControllerData *data)
{
// ===== Write Button Data =====
digitalWrite(PIN_BUTTON_A,data->buttonA);
digitalWrite(PIN_BUTTON_B,data->buttonB);
digitalWrite(PIN_BUTTON_L,data->buttonL);
digitalWrite(PIN_BUTTON_R,data->buttonR);
digitalWrite(PIN_BUTTON_Z,data->buttonZ);
digitalWrite(PIN_BUTTON_S,data->buttonStart);
// ===== Write D-Pad Data =====
digitalWrite(PIN_DPAD_UP, data->DPadUp);
digitalWrite(PIN_DPAD_DOWN, data->DPadDown);
digitalWrite(PIN_DPAD_LEFT, data->DPadLeft);
digitalWrite(PIN_DPAD_RIGHT,data->DPadRight);
// ===== Write C-Button Data =====
digitalWrite(PIN_C_UP, data->CUp);
digitalWrite(PIN_C_DOWN, data->CDown);
digitalWrite(PIN_C_LEFT, data->CLeft);
digitalWrite(PIN_C_RIGHT, data->CRight);
// ===== Write Joystick Data =====
// calculate change in axis between writes
//int xAxisChange = data->xAxis - lastxAxis;
//int yAxisChange = data->yAxis - lastyAxis;
// move axis accordingly
moveAxis(PIN_A_AXIS_X,PIN_B_AXIS_X,mapJoystickToEncoderPos(data->xAxis,data),&encoderXpos);
moveAxis(PIN_A_AXIS_Y,PIN_B_AXIS_Y,mapJoystickToEncoderPos(data->yAxis,data),&encoderYpos);
}

19
Output.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef OUTPUT_H
#define OUTPUT_H
#include "ControllerData.h"
/** Function to output Data stored in ControllerData struct to the iQue console.
* Buttons states are directly set usig digitalWrite(), Axis Data is written by
* Simulating the 90° shifted quadrate encoding used by the N64 joysticks.
* As these operate on a relative basis the difference between the current
* and the last joystick positions are calculated and simulated.
*/
extern void outputToiQue(ControllerData *data);
/** Function to setup the IO used for interfacing with the N64-Controller
* and the iQue-Console
*/
extern void setupIO();
#endif

33
PinMappings.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef PIN_MAPPINGS_H
#define PIN_MAPPINGS_H
#define DATA_PIN 13
#define PIN_BUTTON_A 15
#define PIN_BUTTON_B 5
#define PIN_BUTTON_Z 23
#define PIN_BUTTON_S 1
#define PIN_BUTTON_L 1
#define PIN_BUTTON_R 1
#define PIN_DPAD_UP 1
#define PIN_DPAD_DOWN 1
#define PIN_DPAD_LEFT 1
#define PIN_DPAD_RIGHT 1
#define PIN_C_UP 1
#define PIN_C_DOWN 1
#define PIN_C_LEFT 1
#define PIN_C_RIGHT 1
#define PIN_A_AXIS_X 14
#define PIN_B_AXIS_X 27
#define PIN_A_AXIS_Y 2
#define PIN_B_AXIS_Y 4
#define LINE_WRITE_HIGH pinMode(DATA_PIN,INPUT_PULLUP)
#define LINE_WRITE_LOW pinMode(DATA_PIN,OUTPUT)
#endif

22
README.md Normal file
View File

@ -0,0 +1,22 @@
### What is this Project about ?
In the early 2000s Nintendo released a version of the N64 Console for the Chinese Market called the iQue Player. This Console is much looked after and increasingly hard to come by.
This is made worse by the fact that the inputs, such as the Buttons and the Joystick are hardwired to the Console. As my console began to wear down I sought a way to be able to connect a standart N64 controller to the console. As this was not done yet I created this repository to aid people seeking to do something similar in collecting all the scarce information in one place
### How far along is the Project ?
The current code is working as intended and I played through the first Dungeons of Legen of Zelda - Ocarina of Time without any problems. However the Project is still a mess of wires, and not all Buttons have been hooked up yet. I intend to create a PCB to mount everything back into the origin casing and upload the Eagle Files to this repository, as well as to create assembly instructions and maybe also upload precompiled binaries.
### What Hardware is needed for the Project ?
The code is designed to be run on a ESP32 Microprocessor from Espressif running @ 240Mhz.
### What Software is needed to compile from source ?
The code was compiled using Arduino IDE and version 1.0.4 of the Arduino-ESP32 Core.
As Timing is very critical (a bit read from the controller takes only 4uS !) I decided to not bother with working around the compiler optimizations and decided to disable them alltogether. This means you will have to edit the `platform.txt` file to set optimization level to -O0.
### Will I need to perform manual changes ?
Yes you will have to edit the Pins used in the file `PinMappings.h` to match the Pins you wired everything up with. The Axis pins have to be connected to the correct Wires coming from the Joystick. For that is adviced to cut the Joystick cable in half and connect the X-Axis pins to the pins labeled 1 & 4 of the Joystick PCB and the Y-Axis pins to the pins labeled 5 & 6.

39
iQue.ino Normal file
View File

@ -0,0 +1,39 @@
#include "Output.h"
#include "ControllerData.h"
#include "ControllerBuffer.h"
#include "Debug.h"
ControllerData controller;
void setup()
{
Serial.begin(115200);
// setup io pins
setupIO();
#ifdef PLOT_CONSOLE_POLLING
delay(5000);
sendCommand(0x01);
while(true);
#endif
sendCommand(0x01);
updateOffsetsAndResolution();
}
void loop()
{
// send command 0x01 to n64 controller
sendCommand(0x01);
// store received data in controller struct
populateControllerStruct(&controller);
// output received data to ique
outputToiQue(&controller);
// polling must not occur faster than every 20 ms
delay(14);
}