/* * Copyright (c) 2009 Andrew Brown * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include "pins_arduino.h" #include "../crc_table.h" #include #include #define INPUT_FILENAME "1key.m64" #define SD_SS_PIN 4 #define STATUS_PIN 13 #define SERIAL_BAUD_RATE 9600 #define N64_PIN 8 #define N64_HIGH DDRB &= ~0x01 #define N64_LOW DDRB |= 0x01 #define N64_QUERY (PINB & 0x01) #define LED_HIGH DDRB &= ~0x20 #define LED_LOW DDRB |= 0x20 #define INPUT_BUFFER_SIZE 64 // This is ideal since it allows us to read 256*4/2 = 512 bytes // at once. 512 bytes is an optimization for reading the sd card and skips using another buffer. #define INPUT_BUFFER_UPDATE_TIMEOUT 10 // 10 ms static char n64_raw_dump[38]; // maximum recv is 1+2+32 bytes + 1 bit // n64_raw_dump does /not/ include the command byte. That gets pushed into // n64_command: static unsigned char n64_command; // bytes to send to the 64 // maximum we'll need to send is 33, 32 for a read request and 1 CRC byte static unsigned char n64_buffer[33]; static void get_n64_command(); static void n64_send(); // Simple switch buffer. (If buffer A fails to load while buffer B is in use, // we still okay, and will try again next loop) static unsigned long inputBuffer[INPUT_BUFFER_SIZE]; static bool bufferALoaded, bufferBLoaded; static bool bufferAInUse, bufferBInUse; static int bufferEndPos; static bool bufferOneMore; static long bufferPos; static void updateInputBuffer(); static File m64File; static bool m64OpenSuccess = false; static bool openM64(); static bool finished = false; void setup() { Serial.begin(SERIAL_BAUD_RATE); while (!Serial) { ; } // wait for serial port to connect. Needed for native USB port only Serial.println("Starting up"); // Status LED digitalWrite(STATUS_PIN, LOW); pinMode(STATUS_PIN, OUTPUT); // Communication with the N64 on this pin digitalWrite(N64_PIN, LOW); pinMode(N64_PIN, INPUT); // Initialize SD card if (!SD.begin(SD_SS_PIN)) { m64OpenSuccess = false; Serial.println(F("SD initialization failed!")); return; } Serial.println(F("SD initialization done.")); // Open m64 file if (!openM64()) { m64OpenSuccess = false; return; } m64OpenSuccess = true; // Setup buffer bufferALoaded = false; bufferBLoaded = false; bufferAInUse = false; bufferBInUse = false; bufferEndPos = -1; bufferOneMore = true; bufferPos = -1; updateInputBuffer(); Serial.println(F("Initialization done.")); } void loop() { unsigned char data, addr; unsigned long updateTime; // Loop forever if file open failed if (!m64OpenSuccess) { Serial.print(F("Stopping program due to failed file open...")); while (true) { ; } } // wait to make sure the line is idle before // we begin listening for (int idle_wait=32; idle_wait>0; --idle_wait) { if (!N64_QUERY) { idle_wait = 32; } } noInterrupts(); // Wait for incomming 64 command // this will block until the N64 sends us a command get_n64_command(); // 0x00 is identify command // 0x01 is status // 0x02 is read // 0x03 is write switch (n64_command) { case 0x00: case 0xFF: // identify // mutilate the n64_buffer array with our status // we return 0x050001 to indicate we have a rumble pack // or 0x050002 to indicate the expansion slot is empty // // 0xFF I've seen sent from Mario 64 and Shadows of the Empire. // I don't know why it's different, but the controllers seem to // send a set of status bytes afterwards the same as 0x00, and // it won't work without it. n64_buffer[0] = 0x05; n64_buffer[1] = 0x00; n64_buffer[2] = 0x01; n64_send(n64_buffer, 3, 0); interrupts(); Serial.println(F("Controller identified")); break; case 0x01: // If the TAS is finished, there's nothing left to do. if (finished) break; // blast out the pre-assembled array in n64_buffer n64_send((byte *) (inputBuffer + bufferPos), 4, 0); interrupts(); // update input buffer and make sure it doesn't take too long updateTime = micros(); /*Serial.print(F("Pos: ")); Serial.print(bufferPos); Serial.print(F(" Data: ")); Serial.println(inputBuffer[bufferPos]);*/ updateInputBuffer(); // Record if it took longer than expected updateTime = micros() - updateTime; if (updateTime > INPUT_BUFFER_UPDATE_TIMEOUT * 1000) { Serial.print(F("Input buffer update took to long (")); Serial.print(updateTime / 1000); Serial.println(F(" ms)")); } break; case 0x02: // A read. If the address is 0x8000, return 32 bytes of 0x80 bytes, // and a CRC byte. this tells the system our attached controller // pack is a rumble pack // Assume it's a read for 0x8000, which is the only thing it should // be requesting anyways memset(n64_buffer, 0x80, 32); n64_buffer[32] = 0xB8; // CRC n64_send(n64_buffer, 33, 1); interrupts(); Serial.println(F("Got a read, what?")); //Serial.println("It was 0x02: the read command"); break; case 0x03: // A write. we at least need to respond with a single CRC byte. If // the write was to address 0xC000 and the data was 0x01, turn on // rumble! All other write addresses are ignored. (but we still // need to return a CRC) // decode the first data byte (fourth overall byte), bits indexed // at 24 through 31 data = 0; data |= (n64_raw_dump[16] != 0) << 7; data |= (n64_raw_dump[17] != 0) << 6; data |= (n64_raw_dump[18] != 0) << 5; data |= (n64_raw_dump[19] != 0) << 4; data |= (n64_raw_dump[20] != 0) << 3; data |= (n64_raw_dump[21] != 0) << 2; data |= (n64_raw_dump[22] != 0) << 1; data |= (n64_raw_dump[23] != 0); // get crc byte, invert it, as per the protocol for // having a memory card attached n64_buffer[0] = crc_repeating_table[data] ^ 0xFF; // send it n64_send(n64_buffer, 1, 1); interrupts(); Serial.println(F("Got a write, what?")); // end of time critical code // was the address the rumble latch at 0xC000? // decode the first half of the address, bits // 8 through 15 addr = 0; addr |= (n64_raw_dump[0] != 0) << 7; addr |= (n64_raw_dump[1] != 0) << 6; addr |= (n64_raw_dump[2] != 0) << 5; addr |= (n64_raw_dump[3] != 0) << 4; addr |= (n64_raw_dump[4] != 0) << 3; addr |= (n64_raw_dump[5] != 0) << 2; addr |= (n64_raw_dump[6] != 0) << 1; addr |= (n64_raw_dump[7] != 0); //Serial.println("It was 0x03: the write command"); //Serial.print("Addr was 0x"); //Serial.print(addr, HEX); //Serial.print(" and data was 0x"); //Serial.println(data, HEX); break; default: interrupts(); Serial.print(F("Unknown command: 0x")); Serial.println(n64_command, HEX); break; } } static bool openM64() { char signature[4]; int version; // Open the file for reading: Serial.println(F("Opening file '" INPUT_FILENAME "'...")); m64File = SD.open(F(INPUT_FILENAME)); // Error check if (!m64File) { Serial.println(F("Error in opening file")); return false; } // Open header if (m64File.read(signature, 4) != 4 || m64File.read(&version, 4) != 4) { m64File.close(); return false; } // Validate file signature if (memcmp(signature, "M64\x1A", 4) != 0) { Serial.println(F("File is not an m64 file or an error occured while reading.")); m64File.close(); return false; } // Print version Serial.print(F("M64 Version: ")); Serial.println(version); // Get header size switch(version) { case 1: case 2: m64File.seek(0x200); break; case 3: m64File.seek(0x400); break; default: // Unknown version Serial.println(F("Error: uknown M64 version")); m64File.close(); return false; } // Final check if (!m64File.available()) { Serial.println(F("No input data found in file")); m64File.close(); return false; } Serial.println(F("File opened succesfully")); return true; } static void updateInputBuffer() { int readBytes = 0; if (bufferPos == -1) { // Initially, both of our buffers are not in use bufferPos = 0; bufferAInUse = false; bufferBInUse = false; } else { // Calculate next buffer position bufferPos++; // Wrap position if (bufferPos >= INPUT_BUFFER_SIZE) bufferPos = 0; // Calculate which buffer (A or B) is in use if (bufferPos < INPUT_BUFFER_SIZE / 2) { // Verify the buffer in use is loaded if (!bufferALoaded) { Serial.println(F("Error: New input was not loaded in buffer.")); } // Clear old buffer when loading if (!bufferAInUse) { bufferBLoaded = false; } bufferAInUse = true; bufferBInUse = false; } else { if (!bufferBLoaded) { Serial.println(F("Error: New input was not loaded in buffer.")); } // Clear old buffer when loading if (!bufferBInUse) { bufferALoaded = false; } bufferAInUse = false; bufferBInUse = true; } } // If we have loaded all inputs, wrap up if (bufferEndPos != -1) { // Perform one more input then end once the last input data has been queued. if (!bufferOneMore) { Serial.println(F("TAS finished playing")); finished = true; } else if (bufferPos == bufferEndPos) bufferOneMore = false; return; } // Check for file end if (!m64File.available()) { Serial.println(F("End of M64 loaded into buffer")); // Set end if (!bufferAInUse) { // End of buffer B marks end when bufferEndPos = INPUT_BUFFER_SIZE - 1; } else if (!bufferBInUse) { // End of buffer A marks end bufferEndPos = INPUT_BUFFER_SIZE/2 - 1; } else { // Da fuq? (This should never happen) Serial.println(F("Unexpected ending of input file")); // End immediatley bufferEndPos = 0; finished = true; } // Close file m64File.close(); return; } // Load buffer A when its not in use if (!bufferALoaded && !bufferAInUse) { //Serial.println(F("Loading inputs into buffer A")); // Read bytes readBytes = m64File.read(inputBuffer, INPUT_BUFFER_SIZE*2); if (readBytes == 0) { Serial.println(F("Failed to read next inputs from file. (This is recoverable)")); } else { bufferALoaded = true; if (readBytes != INPUT_BUFFER_SIZE*2) { Serial.println(F("End of M64 file found. Loading remaining bytes.")); bufferEndPos = readBytes / 4 - 1; // Close file m64File.close(); } } } // Load buffer B when its not in use else if (!bufferBLoaded && !bufferBInUse) { //Serial.println(F("Loading inputs into buffer B")); // Read bytes readBytes = m64File.read(inputBuffer + (INPUT_BUFFER_SIZE / 2), INPUT_BUFFER_SIZE*2); if (readBytes == 0) { Serial.println(F("Failed to read next inputs from file. (This is recoverable)")); } else { bufferBLoaded = true; if (readBytes != INPUT_BUFFER_SIZE*2) { Serial.println(F("End of M64 file found. Loading remaining bytes.")); // buffer end is offset from middle bufferEndPos = INPUT_BUFFER_SIZE / 2 + readBytes / 4 - 1; // Close file m64File.close(); } } } } /** * Complete copy and paste of gc_send, but with the N64 * pin being manipulated instead. */ static void n64_send(unsigned char *buffer, char length, bool wide_stop) { asm volatile (";Starting N64 Send Routine"); // Send these bytes char bits; // 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 // take another 3 off for the wide_stop check // 16-2-3=11 // nop block 6 asm volatile ("nop\nnop\nnop\nnop\nnop\n" "nop\nnop\nnop\nnop\nnop\n" "nop\n"); if (wide_stop) { asm volatile (";another 1us for extra wide stop bit\n" "nop\nnop\nnop\nnop\nnop\n" "nop\nnop\nnop\nnop\nnop\n" "nop\nnop\nnop\nnop\n"); } N64_HIGH; } /** * Waits for an incomming signal on the N64 pin and reads the command, * and if necessary, any trailing bytes. * 0x00 is an identify request * 0x01 is a status request * 0x02 is a controller pack read * 0x03 is a controller pack write * * for 0x02 and 0x03, additional data is passed in after the command byte, * which is also read by this function. * * All data is raw dumped to the n64_raw_dump array, 1 bit per byte, except * for the command byte, which is placed all packed into n64_command */ static void get_n64_command() { int bitcount; char *bitbin = n64_raw_dump; n64_command = 0; bitcount = 8; read_loop: // wait for the line to go low while (N64_QUERY){} // 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" ); if (N64_QUERY) n64_command |= 0x01; --bitcount; if (bitcount == 0) goto read_more; n64_command <<= 1; // wait for line to go high again // I don't want this to execute if the loop is exiting, so // I couldn't use a traditional for-loop while (!N64_QUERY) {} goto read_loop; read_more: switch (n64_command) { case (0x03): // write command // we expect a 2 byte address and 32 bytes of data bitcount = 272 + 1; // 34 bytes * 8 bits per byte //Serial.println("command is 0x03, write"); break; case (0x02): // read command 0x02 // we expect a 2 byte address bitcount = 16 + 1; //Serial.println("command is 0x02, read"); break; case (0x00): case (0x01): default: // get the last (stop) bit bitcount = 1; break; } // make sure the line is high. Hopefully we didn't already // miss the high-to-low transition while (!N64_QUERY) {} read_loop2: // wait for the line to go low while (N64_QUERY){} // 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 while (!N64_QUERY) {} goto read_loop2; }