Hello, everyone! I'm working with the following code to communicate with an N64 controller:
#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.
#define N64_HIGH N64_PIN_DIR &= ~0x04
#define N64_LOW N64_PIN_DIR |= 0x04
#define N64_QUERY (PIND & 0x04)
I would like to modify the code so I can use these macros with configurable pins. Timing is critical -- I need to know exactly how many cycles each operation takes, because the N64 controller protocol is based around pulses that are 4 microseconds wide.
If you're interested, the rest of the code is on my Github page
Macros take zero cycles - they are evaluated at compile time, actually BEFORE the code is compiled, so you cannot use them for making run-time pin assignments.
Regards,
Ray L.
Hmm, it sounds like I should find a way of achieving this without using macros. Could someone help me understand what's really going on here, and write a function that would achieve the same thing?
I'm aware that I need to be careful, because the N64 controller is a 3.3V device, and the Arduino is 5V, so there's a possibility of frying it.
You could use addresses stored in pointers instead of constants. There are functions in the Arduino core to translate from Arduino Pin to port and bit.
uint8_t *N64_DDR = DDRD; // Port D
uint8_t *N64_PIN = PIND; // Port D
uint8_t N64_MASK = 0x04; // Pin 2
#define N64_HIGH (N64_DDR &= ~N64_MASK)
#define N64_LOW (N64_DDR |= N64_MASK)
#define N64_QUERY (*N64_PIN & N64_MASK)
Since the values are no longer compile-time constants the operations will take longer. They have to fetch pointers and data from SRAM and store temporary values in registers. You will have to look at the disassembly to determine the cycle count. Four microseconds is 64 instruction cycles so it might be OK.
There are a lot of posts on Google about the N64 controller and the Arduino. Try looking at those.
You can get quite a bit done in 4 µS - after all that is 64 clock cycles.
Ah yes, I thought that sounded familiar. I had done some work on the blasted thing myself. See below:
/*
Nintendo N64 game controller interface.
Author: Nick Gammon
Date: 14 October 2014
Helpful information:
http://afermiano.com/index.php/n64-controller-protocol
https://code.google.com/p/micro-64-controller/wiki/Protocol
Note: Connect the controller data line to D2 via a pullup resistor
(I used 4.7 k) to the 3.3 V pin.
Released for public use.
*/
#include <avr/wdt.h>
#include <avr/builtins.h>
#define CONTROLLER false // true to emulator controller, false to emulate console
#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() N64_PIN_DIR &= ~0x04
#define N64_low() N64_PIN_DIR |= 0x04
#define N64_query() (PIND & 0x04)
#define nop asm volatile ("nop\n\t")
#define WANT_8MHZ false
// status data1:
const byte BUTTON_D_RIGHT = 0x01;
const byte BUTTON_D_LEFT = 0x02;
const byte BUTTON_D_DOWN = 0x04;
const byte BUTTON_D_UP = 0x08;
const byte BUTTON_START = 0x10;
const byte BUTTON_Z = 0x20;
const byte BUTTON_B = 0x40;
const byte BUTTON_A = 0x80;
// status data2:
const byte BUTTON_C_RIGHT = 0x01;
const byte BUTTON_C_LEFT = 0x02;
const byte BUTTON_C_DOWN = 0x04;
const byte BUTTON_C_UP = 0x08;
const byte BUTTON_R = 0x10;
const byte BUTTON_L = 0x20;
// 8 bytes of data that we get from the controller
typedef struct {
byte data1;
byte data2;
char stick_x;
char stick_y;
} N64_status;
void N64_send (byte * output, byte length)
{
byte bitOn;
byte bits;
while (length--)
{
PINB = bit (5); // toggle D13
byte currentByte = *output++;
for (bits = 0; bits < 8; bits++) // 5 cycles if staying in loop
{
bitOn = currentByte & 0x80; // 2 cycles
currentByte <<= 1; // 1 cycle
// ------- low for 1 uS
PINB = bit (3); // toggle D11
N64_low(); // start bit
// we want 14 but subtract 2 for toggling the LED
// and another 2 for the decision below
#if WANT_8MHZ
nop; nop; nop; // 3
#else
nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // 11
#endif
// -------------------
// -------low or high for 2 uS
if (bitOn) // test bit, 1 if high, 2 if low
{
N64_high(); // go high for one - 2 cycles
}
else
{
N64_low(); // stay low for zero - 2 cycles
nop; // compensate for branch - 1 cycle
}
#if WANT_8MHZ
nop; nop; nop; nop; nop; nop; nop; nop; // 8
nop; nop; // 10
#else
nop; nop; nop; nop; nop; nop; nop; nop; // 8
nop; nop; nop; nop; nop; nop; nop; nop; // 16
nop; nop; nop; nop; nop; nop; nop; nop; // 24
nop; nop; // 26
#endif
// -------------------
// ------- high for 1 uS
N64_high(); // stop bit must be high
// we want 14 but subtract 7 because of loop overhead
#if WANT_8MHZ
#else
nop; nop; nop; nop; nop; nop; nop; nop; // 7
#endif
// -------------------
} // end of each bit
}
// console stop bit - low for 1 uS then high
N64_low(); // start bit (2 cycles)
#if WANT_8MHZ
nop; nop; nop; nop; nop; nop; // 6
#else
nop; nop; nop; nop; nop; nop; nop; nop; // 8
nop; nop; nop; nop; nop; nop; // 14
#endif
N64_high();
} // end of N64_send
void N64_get(byte * output, byte length)
{
byte bits;
// controller will time out more slowly
wdt_reset(); // pat the dog
#if CONTROLLER
wdt_enable(WDTO_4S);
#else
// wdt_enable(WDTO_30MS);
wdt_enable(WDTO_8S); // bootloader problems, arrgh!
#endif
while (length--)
{
PINB = bit (5); // toggle D13
for (bits = 0; bits < 8; bits++)
{
// wait for start bit
while (N64_query()) { }
PINB = bit (3); // toggle D11
// wait for 2 uS then check the line
#if WANT_8MHZ
nop; nop; nop; nop; nop; nop; nop; nop; // 8
nop; nop; // 10
#else
nop; nop; nop; nop; nop; nop; nop; nop; // 8
nop; nop; nop; nop; nop; nop; nop; nop; // 16
nop; nop; nop; nop; nop; nop; nop; nop; // 24
nop; nop; nop; nop; // 28
#endif
PINB = bit (4); // toggle D12
// shift left as the most significant bit comes first
*output <<= 1;
// or this bit in
*output |= N64_query() != 0;
// wait for line to go high again
while (!N64_query()) { }
} // end of for each bit
output++;
} // end of while each byte
// wait for stop bit
while (N64_query()) { }
// then other end should let line go high
while (!N64_query()) { }
wdt_disable(); // disable watchdog
} // end of N64_get
byte buf [4];
void setup ()
{
pinMode (N64_PIN, INPUT); // do not make OUTPUT or INPUT_PULLUP!
Serial.begin (115200);
Serial.println ("Starting ...");
// debugging
pinMode (10, OUTPUT);
pinMode (11, OUTPUT);
pinMode (12, OUTPUT);
pinMode (13, OUTPUT);
} // end of setup
void loop ()
{
PINB = bit (2); // toggle D10
#if CONTROLLER
noInterrupts ();
N64_get (buf, 1); // wait for query
byte sendBuf [] = { 0x05, 0x00, 0x02 };
N64_send (sendBuf, sizeof sendBuf);
interrupts ();
#else
byte sendBuf [1] = { 0x00 }; // identify controller
/*
noInterrupts ();
N64_send (sendBuf, sizeof sendBuf);
N64_get (buf, 3); // get ID
interrupts ();
char printBuf [30];
sprintf (printBuf, "ID: %02X %02X %02X", buf [0], buf [1], buf [2]);
Serial.println (printBuf);
Serial.flush ();
*/
noInterrupts ();
sendBuf [0] = 0x01; // poll controller
N64_send (sendBuf, sizeof sendBuf);
N64_status status;
N64_get ((byte *) &status, sizeof status); // get ID
interrupts ();
static N64_status oldStatus;
if (memcmp (&status, &oldStatus, sizeof status) != 0)
{
if (status.stick_x || oldStatus.stick_x)
{
Serial.print ("X: ");
Serial.print ((int) status.stick_x);
Serial.print (" ");
}
if (status.stick_y || oldStatus.stick_y)
{
Serial.print ("Y: ");
Serial.print ((int) status.stick_y);
Serial.print (" ");
}
oldStatus = status;
if (status.data2 & BUTTON_C_RIGHT)
Serial.print (F("C right "));
if (status.data2 & BUTTON_C_LEFT)
Serial.print (F("C left "));
if (status.data2 & BUTTON_C_UP)
Serial.print (F("C up "));
if (status.data2 & BUTTON_C_DOWN)
Serial.print (F("C down "));
if (status.data2 & BUTTON_R)
Serial.print (F("R "));
if (status.data2 & BUTTON_L)
Serial.print (F("L "));
if (status.data1 & BUTTON_D_RIGHT)
Serial.print (F("D right "));
if (status.data1 & BUTTON_D_LEFT)
Serial.print (F("D left "));
if (status.data1 & BUTTON_D_UP)
Serial.print (F("D up "));
if (status.data1 & BUTTON_D_DOWN)
Serial.print (F("D down "));
if (status.data1 & BUTTON_START)
Serial.print (F("Start "));
if (status.data1 & BUTTON_Z)
Serial.print (F("Z "));
if (status.data1 & BUTTON_B)
Serial.print (F("B "));
if (status.data1 & BUTTON_A)
Serial.print (F("A "));
if (status.data1 == 0 && status.data2 == 0)
Serial.print (F("(no buttons)"));
Serial.println ();
Serial.flush ();
}
delay (200);
#endif
} // end of loop
It is, admittedly, timing dependent. But still ...
Hi again, Nick! Yes, I've incorporated your work into my project -- I hope that's okay. I'll be sure to give you credit in the final version.
I was just wondering if there were an alternative to using macros to send signals to the controller. Perhaps calling pinMode() instead would work?
You can just type it... But pinMode and digitalWrite are slow.
Indeed, they are a lot slower and will throw the timings out, perhaps by too much.