diff --git a/README.md b/README.md index 5dbb26b..e4d194a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # wireless-n64-controller This project and its documentation is a Work-In-Progress. I'm still working on writing everything down and working out kinks in the design files and prototypes. -**The idea is to publish the design files and software I used to upgrade a cheap, wired N64 controller to a wireless, Bluetooth controller**. Hopefully these published files can assist others in completing similar projects. +**The idea is to publish the design files and software used to upgrade an OEM or cheap, wired N64 controller to a wireless, Bluetooth controller**. Hopefully these published files can assist others in completing similar projects. ## Motivation -When this project was started, no original-form-factor N64 wireless controllers were available for purchase. There were some two-handle controllers, inspired by N64 original controllers, but none with the original three-handle setup. +When this project was started, no original-form-factor bluetooth N64 wireless controllers were available for purchase. There were some two-handle controllers, inspired by N64 original controllers, but none with the original three-handle setup. -The main goal of this project was to upgrade an existing, cheap three-handle N64 wired controller to use bluetooth, so it could be used with a PC and/or Raspberry Pi. +The main goal of this project was to upgrade an existing OEM or cheap three-handle N64 wired controller to use bluetooth, so it could be used with a PC and/or Raspberry Pi. ## Operation diff --git a/building.md b/building.md index 3125d48..756f192 100644 --- a/building.md +++ b/building.md @@ -10,6 +10,7 @@ See the [BOM for more details and some example-component links](BOM.md), but you * Base N64 controller you plan to modify. * I'm not sure how standardized N64 controller internals are, so it is probably a good idea to confirm the measurements of your controller match up with the spacing of the PCB mounting/securing holes. + * OEM controllers use a 6 pin connector compared to the cheaper alternatives which use a 4 pin. Additional steps are required to configure the software as well as modifications to the PCB for an OEM 6 pin controller. * ESP32 microcontroller * AAA terminals * LiPo battery @@ -42,7 +43,13 @@ Alternatively, you could skip using a battery pak and directly plug/wire a batte ### 1. Program the microcontroller You will need to flash the software for this project on your ESP32 microcontroller. After `ESP-IDF` is setup on your PC, you can connect the microcontroller to your PC via USB and build and flash the software. - + +If you are using and OEM controller the software must be updated to enable SIXPIN mode before building: + +``` +input_poll.h - #define SIXPIN_ENABLED 1 +``` + See [software readme](software/README.md) for more details. ### 2. Assemble the components @@ -61,10 +68,38 @@ Components: 4. **2-pin cables**: Solder one 2-pin JST cable to each external board: right trigger, left trigger, and Z board (you should cut down the length of the cable before hand so there isn't a ton of extra slack taking up space). Each board has two pads to solder to, and it doesn't matter which wire is soldered to which pad. You will need to cut or desolder the existing wires running to these boards. -5. **4-pin header**: Solder the 4-pin PH header/connector for `Analog`. I solder this so the connector opening is facing downward. +5. **4-pin/6-pin header**: Solder the 4-pin PH header/connector for non OEM controllers `Analog`. I solder this so the connector opening is facing downward. + If using the OEM controller, the 6-pin connector needs modifying to work with the current PCB until this is updated. Carefully bend two of the pins up so the header/connector fits into the 4 pin position on the PCB. Then two additional wires are required to be soldered from these bent pins to the correct ESP32 Pins on the PCB (6 -> Pin 13,5 -> Pin 35) as per the picture below: + + + + Additionally, for the 6 Pin connectors the wires from the joystick need swapping around in the 6pin header to match the PCB (a small screwdriver can be used to lift the holding clips and release the wires from the connector): + + + + * Pin 1 - X + * Pin 2 - Power + * Pin 3 - Ground + * Pin 4 - XQ + * Pin 5 - Y + * Pin 6 - YQ + + These should be reordered to: + + * Pin 1 - power + * Pin 2 - Y + * Pin 3 - Ground + * Pin 4 - X + * Pin 5 - XQ + * Pin 6 - YQ + + (More information can be found here: https://dpedu.io/article/2015-03-11/nintendo-64-joystick-pinout-arduino) + + + 6. **4-pin cables**: Solder the 4-pin PH cable to the analog joystick board (you should cut down the length of the cable before hand so there isn't a ton of extra slack taking up space). You will need to cut or desolder the existing wires running to this board - note which wires are labeled as V, X, G, and Y before removing them. Take care to solder the appropriate PH cable to the appropriate pad on the joystick board; on my board the pins were X, Gnd, Y, and V+ (from top to bottom) but yours may be different. 7. **Test**: At this point, I do another quick test to make sure the ESP32 is registering analog joystick data (same as `#2` above). diff --git a/images/6pin_header.jpg b/images/6pin_header.jpg new file mode 100644 index 0000000..73f7173 Binary files /dev/null and b/images/6pin_header.jpg differ diff --git a/images/6pin_header_after.jpg b/images/6pin_header_after.jpg new file mode 100644 index 0000000..00e2485 Binary files /dev/null and b/images/6pin_header_after.jpg differ diff --git a/images/6pin_pcb.png b/images/6pin_pcb.png new file mode 100644 index 0000000..9a7684a Binary files /dev/null and b/images/6pin_pcb.png differ diff --git a/software/main/ble_gamepad_main.cpp b/software/main/ble_gamepad_main.cpp index 2352f58..e10486b 100644 --- a/software/main/ble_gamepad_main.cpp +++ b/software/main/ble_gamepad_main.cpp @@ -15,7 +15,6 @@ #include "driver/adc.h" #include "driver/gpio.h" - // LED mode and misc related status indicators uint32_t led_mode = 0; bool bt_connected = false; @@ -23,12 +22,38 @@ bool bt_connected = false; #define STATE_STARTUP 0 #define STATE_CALIBRATION 1 #define STATE_RUNNING 2 + uint32_t current_state = STATE_STARTUP; bool startup_routine_running = true; BleGamepad bleGamepad; xQueueHandle gpio_evt_queue = NULL; // Button-press event queue - +// For 6-pin joysticks: count movement when an edge change is detected +int countx = 0; +int county = 0; +// For 6-pin joysticks: X Interrupt Hook +static void IRAM_ATTR gpiox_isr_handler(void* arg) +{ + uint32_t gpio_num = (uint32_t) arg; + if(gpio_get_level((gpio_num_t) SIXPIN_ANALOG_X) == gpio_get_level((gpio_num_t) SIXPIN_ANALOG_XQ)){ + countx++; + } + else{ + countx--; + } + +} +// For 6-pin joysticks: Y Interrupt Hook +static void IRAM_ATTR gpioy_isr_handler(void* arg) +{ + uint32_t gpio_num = (uint32_t) arg; + if(gpio_get_level((gpio_num_t) SIXPIN_ANALOG_Y) == gpio_get_level((gpio_num_t) SIXPIN_ANALOG_YQ)){ + county--; + } + else{ + county++; + } +} // Setup specified pin number as pulled-down, input pin void setup_input_pin(uint32_t pin) { @@ -57,12 +82,47 @@ void setup_gpio() previousDpadStates[i] = 0; currentDpadStates[i] = 0; } - // Analog in + + // Battery Monitor, Pin also used with 6 pin Joystick, therefore only availble with 4 pin joysticks + if(!SIXPIN_ENABLED){ adc1_config_width(ADC_WIDTH_BIT_12); + adc1_config_channel_atten(ANALOG_BAT, ADC_ATTEN_DB_11); + } + + // For 6-pin joysticks: Joystick setup + if(SIXPIN_ENABLED){ + //zero-initialize the config structure. + gpio_config_t io_conf = {}; + //disable pull-down mode + io_conf.pull_down_en = (gpio_pulldown_t) 0; + //interrupt of any edge + io_conf.intr_type = GPIO_INTR_ANYEDGE; + //bit mask of the pins + io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL; + //set as input mode + io_conf.mode = GPIO_MODE_INPUT; + //disable pull-up mode + io_conf.pull_up_en = (gpio_pullup_t) 0; + gpio_config(&io_conf); + + //SIXPIN Joystick Interrupts and Handler setup + //install gpio isr service + gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); + //hook isr handler for specific gpio pin + gpio_isr_handler_add((gpio_num_t) SIXPIN_ANALOG_X, gpiox_isr_handler, (void*) SIXPIN_ANALOG_X); + //hook isr handler for specific gpio pin + gpio_isr_handler_add((gpio_num_t) SIXPIN_ANALOG_Y, gpioy_isr_handler, (void*) SIXPIN_ANALOG_Y); + + //Adjust max ADC reading for SIXPIN joystick potentiometers + max_x = SIXPIN_ANALOG_MAX; + max_y = SIXPIN_ANALOG_MAX; + center_x = SIXPIN_ANALOG_CENTER; + center_y = SIXPIN_ANALOG_CENTER; + } + else{ adc1_config_channel_atten(ANALOG_X, ADC_ATTEN_DB_11); adc1_config_channel_atten(ANALOG_Y, ADC_ATTEN_DB_11); - adc1_config_channel_atten(ANALOG_BAT, ADC_ATTEN_DB_11); - + } // Other IO gpio_config_t io_conf; // Button feedback LED output @@ -203,30 +263,37 @@ extern "C" { } -// Calibrate analog X-Y inputs and optionally write to persistent storage. Uses initial values as "centered", then monitors min and max values for ~5 seconds to determine range. +// Calibrate analog X-Y inputs and optionally write to persistent storage. Uses initial values as "centered", then monitors min and max values for ~10 seconds to determine range. void calibrate(bool write_to_storage) { const TickType_t xDelay = 10 / portTICK_PERIOD_MS; // 10ms - center_x = get_analog_raw(ANALOG_X); + if (SIXPIN_ENABLED){center_x = countx;} else{center_x = get_analog_raw(ANALOG_X);}; min_x = center_x; max_x = center_x; - center_y = get_analog_raw(ANALOG_Y); + if (SIXPIN_ENABLED){center_y = county;} else{center_y = get_analog_raw(ANALOG_Y);}; min_y = center_y; max_y = center_y; - // Read extreme values for ~5 seconds - for (int i = 0; i < 500; i++) { - uint16_t x = get_analog_raw(ANALOG_X); + // Read extreme values for ~10 seconds + for (int i = 0; i < 1000; i++) { + int32_t x; + if (SIXPIN_ENABLED) {x = countx;} else{x = get_analog_raw(ANALOG_X);}; + if (x < min_x) min_x = x; if (x > max_x) max_x = x; - uint16_t y = get_analog_raw(ANALOG_Y); + int32_t y; + if (SIXPIN_ENABLED) {y = county;} else{y = get_analog_raw(ANALOG_Y);}; + if (y < min_y) min_y = y; if (y > max_y) max_y = y; vTaskDelay(xDelay); } + + center_x = (min_x + max_x)/2; + center_y = (min_y + max_y)/2; printf("Calibration results:\n"); printf("X (left, center, right): %d, %d, %d\n", min_x, center_x, max_x); printf("Y (up, center, down): %d, %d, %d\n", min_y, center_y, max_y); @@ -252,7 +319,8 @@ void app_main(void) // Get initial button state to enter special modes poll_buttons(); - + countx = 0; + county = 0; // Enter calibration mode if `START` is being pressed if (currentButtonStates[6]) { current_state = STATE_CALIBRATION; @@ -273,13 +341,14 @@ void app_main(void) printf(" X: %d, %d, %d\n", min_x, center_x, max_x); printf(" Y: %d, %d, %d\n", min_y, center_y, max_y); } - + printf("Initial setup complete!\n"); startup_routine_running = false; current_state = STATE_RUNNING; setup_poll_task(); - + //Set center to current postition of stick + if (SIXPIN_ENABLED){center_y = county; center_x = countx;} /* Print chip information */ esp_chip_info_t chip_info; esp_chip_info(&chip_info); diff --git a/software/main/input_poll.cpp b/software/main/input_poll.cpp index 7492f9e..20aeaca 100644 --- a/software/main/input_poll.cpp +++ b/software/main/input_poll.cpp @@ -27,9 +27,7 @@ uint32_t buttonPins[NUM_OF_BUTTONS] = { BTN_Z_PIN, BTN_L_PIN, BTN_R_PIN, - BTN_IDK1_PIN, - BTN_IDK2_PIN, - BTN_IDK3_PIN + BTN_IDK2_PIN }; // "Soft" buttons @@ -44,6 +42,7 @@ uint32_t previousButtonStates[NUM_OF_BUTTONS]; uint32_t currentButtonStates[NUM_OF_BUTTONS]; uint32_t previousSoftButtonStates[NUM_OF_SOFT_BUTTONS]; uint32_t currentSoftButtonStates[NUM_OF_SOFT_BUTTONS]; + int16_t previousXState; int16_t currentXState; int16_t previousYState; @@ -57,17 +56,25 @@ uint32_t dpadPins[4] = {25, 27, 26, 33}; // Analog input center and range -uint16_t center_x = ANALOG_CENTER; -uint16_t min_x = ANALOG_MIN; -uint16_t max_x = ANALOG_MAX; -uint16_t center_y = ANALOG_CENTER; -uint16_t min_y = ANALOG_MIN; -uint16_t max_y = ANALOG_MAX; - +int16_t center_x = ANALOG_CENTER; +int16_t min_x = ANALOG_MIN; +int16_t max_x = ANALOG_MAX; +int16_t center_y = ANALOG_CENTER; +int16_t min_y = ANALOG_MIN; +int16_t max_y = ANALOG_MAX; + +// Ensures analog input stays within expected bounds to avoid overflow or reversal +// when mapping to joystick output range. +int16_t clamp(int16_t val, int16_t min_val, int16_t max_val) { + if (val < min_val) return min_val; + if (val > max_val) return max_val; + return val; +} // Scale x from `in` range to `out` range int32_t map(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; + } @@ -84,21 +91,47 @@ uint16_t get_analog_raw(adc1_channel_t pin) { // Convert the provided raw analog value to a joystick value (-32767 to 32767) based on minimum, median, and maximum values. -int16_t analog_to_joystick_value(uint16_t raw, uint16_t min, uint16_t med, uint16_t max) { +int16_t analog_to_joystick_value(int16_t raw, int16_t min, int16_t med, int16_t max) { // Shortcut for min/max values if (raw >= max) return JOYSTICK_MAX; if (raw <= min) return JOYSTICK_MIN; - + if(SIXPIN_ENABLED) + { + //Clamp raw to detected range to prevent overflow + raw = clamp(raw, min, max); + //Update tracking based on center + raw = raw - med; + } // Negative if (raw < med) { - int32_t joystick_val = map(raw, min, med, JOYSTICK_MIN, 0) * ANALOG_OVERSCALE; + int32_t joystick_val = map(raw, min, med, JOYSTICK_MIN, 0) ; + // printf("negative joystick_val val before scaling : %d \n", joystick_val); + if(SIXPIN_ENABLED) + { + joystick_val = joystick_val * SIXPIN_ANALOG_OVERSCALE; + } + else{ + joystick_val = joystick_val * ANALOG_OVERSCALE; + } + //printf("joystick_val val after scaling: %d \n", joystick_val); if (joystick_val < JOYSTICK_MIN) return JOYSTICK_MIN; + return (int16_t) joystick_val; } // Positive - int32_t joystick_val = map(raw, med, max, 0, JOYSTICK_MAX) * ANALOG_OVERSCALE; + int32_t joystick_val = map(raw, med, max, 0, JOYSTICK_MAX); + //printf("positive joystick_val valbefore scaling : %d \n", joystick_val); + if(SIXPIN_ENABLED) + { + joystick_val = joystick_val * SIXPIN_ANALOG_OVERSCALE; + } + else{ + joystick_val = joystick_val * ANALOG_OVERSCALE; + } + //printf("joystick_val val after scaling: %d \n", joystick_val); if (joystick_val > JOYSTICK_MAX) return JOYSTICK_MAX; + return (int16_t) joystick_val; } @@ -182,7 +215,12 @@ bool poll_joystick() { bool changed = false; // X + if(SIXPIN_ENABLED){ + currentXState = analog_to_joystick_value(countx, min_x, center_x, max_x); + } + else{ currentXState = analog_to_joystick_value(get_analog_raw(ANALOG_X), min_x, center_x, max_x); + } if ((currentXState > 0 && currentXState < JOYSTICK_DEADZONE) || (currentXState < 0 && currentXState > (-1 * JOYSTICK_DEADZONE))) { currentXState = 0; } @@ -194,7 +232,12 @@ bool poll_joystick() { } // Y + if(SIXPIN_ENABLED){ + currentYState = analog_to_joystick_value(county, min_y, center_y, max_y); + } + else{ currentYState = analog_to_joystick_value(get_analog_raw(ANALOG_Y), min_y, center_y, max_y); + } if ((currentYState > 0 && currentYState < JOYSTICK_DEADZONE) || (currentYState < 0 && currentYState > (-1 * JOYSTICK_DEADZONE))) { currentYState = 0; } @@ -231,10 +274,11 @@ void input_poll_loop(void* args) bool changed = false; // Battery - if (ENABLE_BATTERY_CHECK) { - bleGamepad.setBatteryLevel(get_battery_level()); + if (!SIXPIN_ENABLED){ + if (ENABLE_BATTERY_CHECK) { + bleGamepad.setBatteryLevel(get_battery_level()); + } } - // Joystick bool joystick_changed = poll_joystick(); changed |= joystick_changed; diff --git a/software/main/input_poll.h b/software/main/input_poll.h index a0da4a3..d9629fe 100644 --- a/software/main/input_poll.h +++ b/software/main/input_poll.h @@ -24,23 +24,45 @@ #define BATTERY_LEVEL_FULL 2355 // Button state for gamepad buttons -#define NUM_OF_BUTTONS_RIGHT 6 -#define NUM_OF_BUTTONS 13 +#define NUM_OF_BUTTONS_RIGHT 5 +#define NUM_OF_BUTTONS 11 // "Soft" buttons are not physical buttons, e.g. could be triggered by button combos, simulating things like SELECT #define NUM_OF_SOFT_BUTTONS 3 + +// 4 PIN Analog Stick config // GPIO 39 #define ANALOG_X ADC1_CHANNEL_3 // GPIO 34 #define ANALOG_Y ADC1_CHANNEL_6 + +// For 6-pin joysticks: OEM analog stick setup - Not actually analog, uses optical sensors monitoring leading edge with inturrupts +//Enable SIXPIN Stick - If set to 0 then four pin analog enabled +#define SIXPIN_ENABLED 0 +#define ESP_INTR_FLAG_DEFAULT 0 +// GPIO 39 +#define SIXPIN_ANALOG_X 39 // GPIO 35 -#define ANALOG_BAT ADC1_CHANNEL_7 +#define SIXPIN_ANALOG_XQ 35 +// GPIO 34 +#define SIXPIN_ANALOG_Y 34 +// GPIO 34 +#define SIXPIN_ANALOG_YQ 13 +#define GPIO_INPUT_PIN_SEL ((1ULL<