commit 7c272f7a9061417b4de4c1ca45f5b9f6412526ef Author: mnzlmstr Date: Sun Apr 12 22:55:15 2020 +0200 Initial commit diff --git a/ControllerBuffer.cpp b/ControllerBuffer.cpp new file mode 100644 index 0000000..dbce583 --- /dev/null +++ b/ControllerBuffer.cpp @@ -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]); + } + + +} \ No newline at end of file diff --git a/ControllerBuffer.h b/ControllerBuffer.h new file mode 100644 index 0000000..1489cde --- /dev/null +++ b/ControllerBuffer.h @@ -0,0 +1,26 @@ +#ifndef CONTROLLER_BUFFER_H +#define CONTROLLER_BUFFER_H + +#include "ControllerData.h" +#include + +#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 \ No newline at end of file diff --git a/ControllerData.h b/ControllerData.h new file mode 100644 index 0000000..f546ed3 --- /dev/null +++ b/ControllerData.h @@ -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 \ No newline at end of file diff --git a/Debug.h b/Debug.h new file mode 100644 index 0000000..0775613 --- /dev/null +++ b/Debug.h @@ -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 \ No newline at end of file diff --git a/Output.cpp b/Output.cpp new file mode 100644 index 0000000..9e00bca --- /dev/null +++ b/Output.cpp @@ -0,0 +1,135 @@ +#include "Output.h" +#include "PinMappings.h" + +#include + +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); +} \ No newline at end of file diff --git a/Output.h b/Output.h new file mode 100644 index 0000000..6e71579 --- /dev/null +++ b/Output.h @@ -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 \ No newline at end of file diff --git a/PinMappings.h b/PinMappings.h new file mode 100644 index 0000000..67746ce --- /dev/null +++ b/PinMappings.h @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d34fe1 --- /dev/null +++ b/README.md @@ -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. + diff --git a/iQue.ino b/iQue.ino new file mode 100644 index 0000000..e63c010 --- /dev/null +++ b/iQue.ino @@ -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); +} \ No newline at end of file