Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e0249ba
Added first cut of SIXPIN Joysitck
craigmillard86 Feb 28, 2023
5550138
Improved Commenting
craigmillard86 Feb 28, 2023
2f12ad6
Updated to utilise scaling and calibration
craigmillard86 Mar 5, 2023
3366711
Refactored and added Calibration for SIXPIN
craigmillard86 Mar 6, 2023
1a3dab2
Delete software/components/esp-nimble-cpp-1.3.1/src directory
craigmillard86 Mar 6, 2023
4f567d8
Update to resolve PR comments
craigmillard86 Mar 31, 2023
28c5482
Disabled SIXPIN
craigmillard86 Mar 31, 2023
5852a34
updated comments and defaults
craigmillard86 Apr 1, 2023
a0dfb98
Updated main README
craigmillard86 Apr 1, 2023
0092fb6
Updated build readme
craigmillard86 Apr 1, 2023
32a2ae5
updated build readme
craigmillard86 Apr 1, 2023
d4f2d97
Updated Build readme
craigmillard86 Apr 1, 2023
95cd054
Upload image joystick PCB
craigmillard86 Apr 1, 2023
38d6c64
Merge branch '6pin-n64' of https://github.com/craigmillard86/wireless…
craigmillard86 Apr 1, 2023
16c44ae
Update building.md
craigmillard86 Apr 1, 2023
cf555bc
Update building.md
craigmillard86 Apr 1, 2023
3ff2123
Update Images for 6pin
craigmillard86 Apr 1, 2023
2196e13
Update building.md
craigmillard86 Apr 1, 2023
9ddc7eb
Disabled Bat functionality if SIXPIN is enabled
craigmillard86 Apr 2, 2023
48848e3
Updated to use abs() vs custom function
craigmillard86 Apr 2, 2023
7eac740
Updated to remove factor* and sixpin calibration
craigmillard86 Apr 26, 2023
f2c74da
Updated based on PR Feedback
craigmillard86 May 1, 2023
cd0f513
Update building.md
craigmillard86 May 1, 2023
6567e20
Update input_poll.h
craigmillard86 May 27, 2023
0982514
Updated SIXPIN conversion handelling to resolve overflow bugs
craigmillard86 Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<img src=images/controller_prototype.jpg width=1024>

## 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

Expand Down
39 changes: 37 additions & 2 deletions building.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.

<img src=images/4pin_header.jpg width=360>

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:

<img src=images/6pin_header.jpg width=360>

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):

<img src=images/6pin_pcb.png width=360>

* 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)

<img src=images/6pin_header_after.jpg width=360>

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).
Expand Down
Binary file added images/6pin_header.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/6pin_header_after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/6pin_pcb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 84 additions & 15 deletions software/main/ble_gamepad_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,45 @@
#include "driver/adc.h"
#include "driver/gpio.h"


// LED mode and misc related status indicators
uint32_t led_mode = 0;
bool bt_connected = false;
// Controller state-machine
#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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);
Expand Down
78 changes: 61 additions & 17 deletions software/main/input_poll.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;

}


Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Loading