Hi,
I'm a software engineer by trade but this is my first foray into electronics so I apologise in advance for any noobiness.
I've been trying to build a USB interface for an N64 controller. There are various implementations of this around the web primarily using arduino boards.
The code* (attached below) should output the status of all the buttons of the N64 controller (using pin 2 / port 0x04 to read). This runs on an Arduino Nano just fine - except that the Nano doesn't support USB HID so that's defunct. But the code is stable (once the references to the Joystick library are removed of course).
The serial monitor read looks like this and updates every 10ms:
When it comes to implementing this on a Teensy LC or a Sparkfun Pro Micro, which both support USB HID, the button 'read' appears unstable. That is, if I don't touch anything on the controller, a couple of the buttons (particularly Dup and Ddown) flicker quickly from 0 to 1 and back. If I hold a button down on the controller, it is output correctly but flickers from 1 to 0 and back.
As the code/serial monitor/button press is rock solid on the Nano, it doesn't feel like a bug in the code as such. I am leaning towards a difference in the Mhz speed of the boards since the read of the controller is tightly timed. Is that possible? I noticed the flicker seemed slightly different when running at 48mhz vs 24mhz. I did try a 16mhz underclock I found, but this wouldn't compile afterwards. Complaints about PluggableUSB libraries not being found.
I feel so close - if anyone can help point me in the right direction, it'd be much appreciated.
Thanks!
*Slight modifications by me; namely the TeensyLC Joystick partial implementation
Setup:
Teensy LC / Sparkfun Pro Micro / Arduino Nano
Arduino IDE
Windows 10 x64
I'm a software engineer by trade but this is my first foray into electronics so I apologise in advance for any noobiness.
I've been trying to build a USB interface for an N64 controller. There are various implementations of this around the web primarily using arduino boards.
The code* (attached below) should output the status of all the buttons of the N64 controller (using pin 2 / port 0x04 to read). This runs on an Arduino Nano just fine - except that the Nano doesn't support USB HID so that's defunct. But the code is stable (once the references to the Joystick library are removed of course).
The serial monitor read looks like this and updates every 10ms:
Code:
Start: 0
Z: 0
B: 0
A: 0
L: 0
R: 0
Cup: 0
Cdown: 0
Cright:0
Cleft: 0
Dup: 0
Ddown: 0
Dright:0
Dleft: 0
Stick X:0
Stick Y:0
Stick Reset:0
When it comes to implementing this on a Teensy LC or a Sparkfun Pro Micro, which both support USB HID, the button 'read' appears unstable. That is, if I don't touch anything on the controller, a couple of the buttons (particularly Dup and Ddown) flicker quickly from 0 to 1 and back. If I hold a button down on the controller, it is output correctly but flickers from 1 to 0 and back.
As the code/serial monitor/button press is rock solid on the Nano, it doesn't feel like a bug in the code as such. I am leaning towards a difference in the Mhz speed of the boards since the read of the controller is tightly timed. Is that possible? I noticed the flicker seemed slightly different when running at 48mhz vs 24mhz. I did try a 16mhz underclock I found, but this wouldn't compile afterwards. Complaints about PluggableUSB libraries not being found.
I feel so close - if anyone can help point me in the right direction, it'd be much appreciated.
Thanks!
Code:
/**
* Gamecube controller to Nintendo 64 adapter
* by Andrew Brown
* Rewritten for N64 to HID by Peter Den Hartog
*/
/**
* To use, hook up the following to the Arduino Duemilanove:
* Digital I/O 2: N64 serial line
* All appropriate grounding and power lines
*/
#include "pins_arduino.h"
#define N64_PIN 2
#define N64_PIN_DIR DDRD
// these two macros set arduino pin 2 to input or output, which with an
// external 1K pull-up resistor to the 3.3V rail, is like pulling it high or
// low. These operations translate to 1 op code, which takes 2 cycles
#define N64_HIGH DDRD &= ~0x4
#define N64_LOW DDRD |= 0x4
#define N64_QUERY (PIND & 0x4)
// 8 bytes of data that we get from the controller
struct {
// bits: 0, 0, 0, start, y, x, b, a
unsigned char data1;
// bits: 1, L, R, Z, Dup, Ddown, Dright, Dleft
unsigned char data2;
char stick_x;
char stick_y;
} N64_status;
char N64_raw_dump[33]; // 1 received bit per byte
void N64_send(unsigned char *buffer, char length);
void N64_get();
void print_N64_status();
void translate_raw_data();
#include "crc_table.h"
void setup()
{
Serial.begin(115200);
Joystick.useManualSend(true);
// Communication with gamecube controller on this pin
// Don't remove these lines, we don't want to push +5V to the controller
digitalWrite(N64_PIN, LOW);
pinMode(N64_PIN, INPUT);
// Initialize the gamecube controller by sending it a null byte.
// This is unnecessary for a standard controller, but is required for the
// Wavebird.
unsigned char initialize = 0x00;
noInterrupts();
N64_send(&initialize, 1);
// Stupid routine to wait for the gamecube controller to stop
// sending its response. We don't care what it is, but we
// can't start asking for status if it's still responding
int x;
for (x=0; x<64; x++) {
// make sure the line is idle for 64 iterations, should
// be plenty.
if (!N64_QUERY)
x = 0;
}
// Query for the gamecube controller's status. We do this
// to get the 0 point for the control stick.
unsigned char command[] = {0x01};
N64_send(command, 1);
// read in data and dump it to N64_raw_dump
N64_get();
interrupts();
translate_raw_data();
/*
*/
}
void translate_raw_data()
{
// The get_N64_status function sloppily dumps its data 1 bit per byte
// into the get_status_extended char array. It's our job to go through
// that and put each piece neatly into the struct N64_status
int i;
memset(&N64_status, 0, sizeof(N64_status));
// line 1
// bits: A, B, Z, Start, Dup, Ddown, Dleft, Dright
for (i=0; i<8; i++) {
N64_status.data1 |= N64_raw_dump[i] ? (0x80 >> i) : 0;
}
// line 2
// bits: 0, 0, L, R, Cup, Cdown, Cleft, Cright
for (i=0; i<8; i++) {
N64_status.data2 |= N64_raw_dump[8+i] ? (0x80 >> i) : 0;
}
// line 3
// bits: joystick x value
// These are 8 bit values centered at 0x80 (128)
for (i=0; i<8; i++) {
N64_status.stick_x |= N64_raw_dump[16+i] ? (0x80 >> i) : 0;
}
for (i=0; i<8; i++) {
N64_status.stick_y |= N64_raw_dump[24+i] ? (0x80 >> i) : 0;
}
}
/**
* This sends the given byte sequence to the controller
* length must be at least 1
* Oh, it destroys the buffer passed in as it writes it
*/
void N64_send(unsigned char *buffer, char length)
{
// Send these bytes
char bits;
//bool bit;
// This routine is very carefully timed by examining the assembly output.
// Do not change any statements, it could throw the timings off
//
// We get 16 cycles per microsecond, which should be plenty, but we need to
// be conservative. Most assembly ops take 1 cycle, but a few take 2
//
// I use manually constructed for-loops out of gotos so I have more control
// over the outputted assembly. I can insert nops where it was impossible
// with a for loop
asm volatile ("#Starting outer for loop");
outer_loop:
{
asm volatile ("#Starting inner for loop");
bits=8;
inner_loop:
{
// Starting a bit, set the line low
asm volatile ("#Setting line to low");
N64_LOW; // 1 op, 2 cycles
asm volatile ("#branching");
if (*buffer >> 7) {
asm volatile ("#Bit is a 1");
// 1 bit
// remain low for 1us, then go high for 3us
// nop block 1
asm volatile ("nop\nnop\nnop\nnop\nnop\n");
asm volatile ("#Setting line to high");
N64_HIGH;
// nop block 2
// we'll wait only 2us to sync up with both conditions
// at the bottom of the if statement
asm volatile ("nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
);
} else {
asm volatile ("#Bit is a 0");
// 0 bit
// remain low for 3us, then go high for 1us
// nop block 3
asm volatile ("nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\n");
asm volatile ("#Setting line to high");
N64_HIGH;
// wait for 1us
asm volatile ("#end of conditional branch, need to wait 1us more before next bit");
}
// end of the if, the line is high and needs to remain
// high for exactly 16 more cycles, regardless of the previous
// branch path
asm volatile ("#finishing inner loop body");
--bits;
if (bits != 0) {
// nop block 4
// this block is why a for loop was impossible
asm volatile ("nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\n");
// rotate bits
asm volatile ("#rotating out bits");
*buffer <<= 1;
goto inner_loop;
} // fall out of inner loop
}
asm volatile ("#continuing outer loop");
// In this case: the inner loop exits and the outer loop iterates,
// there are /exactly/ 16 cycles taken up by the necessary operations.
// So no nops are needed here (that was lucky!)
--length;
if (length != 0) {
++buffer;
goto outer_loop;
} // fall out of outer loop
}
// send a single stop (1) bit
// nop block 5
asm volatile ("nop\nnop\nnop\nnop\n");
N64_LOW;
// wait 1 us, 16 cycles, then raise the line
// 16-2=14
// nop block 6
asm volatile ("nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\n");
N64_HIGH;
}
void N64_get()
{
// listen for the expected 8 bytes of data back from the controller and
// blast it out to the N64_raw_dump array, one bit per byte for extra speed.
// Afterwards, call translate_raw_data() to interpret the raw data and pack
// it into the N64_status struct.
asm volatile ("#Starting to listen");
unsigned char timeout;
char bitcount = 32;
char *bitbin = N64_raw_dump;
// Again, using gotos here to make the assembly more predictable and
// optimization easier (please don't kill me)
read_loop:
timeout = 0x3f;
// wait for line to go low
while (N64_QUERY) {
if (!--timeout)
return;
}
// wait approx 2us and poll the line
asm volatile (
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
"nop\nnop\nnop\nnop\nnop\n"
);
*bitbin = N64_QUERY;
++bitbin;
--bitcount;
if (bitcount == 0)
return;
// wait for line to go high again
// it may already be high, so this should just drop through
timeout = 0x3f;
while (!N64_QUERY) {
if (!--timeout)
return;
}
goto read_loop;
}
void print_N64_status()
{
//int i;
// bits: A, B, Z, Start, Dup, Ddown, Dleft, Dright
// bits: 0, 0, L, R, Cup, Cdown, Cleft, Cright
Serial.println();
Serial.print("Start: ");
Serial.println(N64_status.data1 & 16 ? 1:0);
Serial.print("Z: ");
Serial.println(N64_status.data1 & 32 ? 1:0);
Serial.print("B: ");
Serial.println(N64_status.data1 & 64 ? 1:0);
Serial.print("A: ");
Serial.println(N64_status.data1 & 128 ? 1:0);
Serial.print("L: ");
Serial.println(N64_status.data2 & 32 ? 1:0);
Serial.print("R: ");
Serial.println(N64_status.data2 & 16 ? 1:0);
Serial.print("Cup: ");
Serial.println(N64_status.data2 & 0x08 ? 1:0);
Serial.print("Cdown: ");
Serial.println(N64_status.data2 & 0x04 ? 1:0);
Serial.print("Cright:");
Serial.println(N64_status.data2 & 0x01 ? 1:0);
Serial.print("Cleft: ");
Serial.println(N64_status.data2 & 0x02 ? 1:0);
Serial.print("Dup: ");
Serial.println(N64_status.data1 & 0x08 ? 1:0);
Serial.print("Ddown: ");
Serial.println(N64_status.data1 & 0x04 ? 1:0);
Serial.print("Dright:");
Serial.println(N64_status.data1 & 0x01 ? 1:0);
Serial.print("Dleft: ");
Serial.println(N64_status.data1 & 0x02 ? 1:0);
Serial.print("Stick X:");
Serial.println(N64_status.stick_x, DEC);
Serial.print("Stick Y:");
Serial.println(N64_status.stick_y, DEC);
Serial.print("Stick Reset:");
Serial.println(N64_status.data2 & 128 ? 1:0);
}
void loop()
{
//int i;
//unsigned char data;
//unsigned char addr;
// Command to send to the gamecube
// The last bit is rumble, flip it to rumble
// yes this does need to be inside the loop, the
// array gets mutilated when it goes through N64_send
unsigned char command[] = {0x01};
// don't want interrupts getting in the way
noInterrupts();
// send those 3 bytes
N64_send(command, 1);
// read in data and dump it to N64_raw_dump
N64_get();
// end of time sensitive code
interrupts();
// translate the data in N64_raw_dump to something useful
translate_raw_data();
//Start:
Joystick.button(1, N64_status.data1 & 16 ? 1 : 0);
Joystick.X(N64_status.stick_x);
//Data for the Y axis seems to come out inverted
Joystick.Y(N64_status.stick_y * -1);
Joystick.send_now();
// DEBUG: print it
print_N64_status();
delay(10);
/*
*/
}
Setup:
Teensy LC / Sparkfun Pro Micro / Arduino Nano
Arduino IDE
Windows 10 x64