|
|
|
Old school Nintendo will always be part of my generation's past time.
It was my first gaming system and I still remember the day that my parents
brought it home. I recently became interested in acquiring a Nintendo for
old time reminicing. Sure, I have an XBOX, but its just not the same.
While I was looking online and at stores, I noticed that it was hard,
if not impossible, to find a lot of my favorite games. This is what
started my interest in NES emulators and ROM's for the PC. This seemed like
the easiest and best solution. I had all of the games at my disposal and I
never had to blow on any of the cartridges to get them to work.
There was always one thing that bothered me about playing on my computer though;
it just doesn't have the same feel without the controllers. I looked around
online and discovered companies that created adapters to connect a controller
up to the computer, but they were all either ridiculously overpriced or used a
serial connector (my computer does not have any serial ports.. All USB). This
is why I started to seek out a way to inexpensively connect an authentic Nintendo
Controller to my PC. Having recently graduated with my Bachelors of Sciencce in
Computer Engineering, this seemed like a pretty trivial task.
For those interested, I will go through all of the steps to create your very own
USB NES Controller. However, if you are not interested in HOW it is done
and just want the HEX files and the schematic, you can skip to the end.
The first thing I did was order some old controllers off of EBay and tear them apart.
I was a little surprised at the simplicity of the controllers, 5 wires and one IC.
Upon further examination, I noticed that the IC was just a CD4021 - 8 Bit parallel/serial in,
serial out shift register.
[CD4021 Datasheet]

Controller Schematic
From the datasheet and the schematic, you can see that all of the buttons are active low.
Furthermore, the order of the data is, from the MSB: A, B, Start, Select, Up, Down, Left, Right.
The pins for the NES controller are now apparent, Data, Clock, Latch, Vdd, and Vss. Before we
get too far ahead of ourselves here, lets get the controller working with a simple PIC chip.
A Microchip PIC is just a microcontroller, any microcontroller will work, I happen to prefer the
PIC's myself, but you really could use just about ANY microcontroller. We need to generate
some waveforms for the controller to send us back data. Namely, we need to pulse the latch pin
and then clock the data out of the shift register. The waveform should look something like this:
Again, for prototyping I hooked the controller up to a PIC18F452. The code to read the controller
and output the keys to the serial port is shown below. It is quite simple, the code just scans the
controller and outputs the results to the serial port.
[Full Source Code]
|
output_bit(NES_LATCH, 1);
delay_ms(1);
output_bit(NES_LATCH, 0);
ConState = 0;
for(n=0; n <8; n++)
{
shift_right(&ConState, 1, !input(NES_DATA));
delay_ms(1);
output_bit(NES_CLK, 1);
delay_ms(1);
output_bit(NES_CLK, 0);
} |
|
Before we get too much further, I want to talk
just a little bit about how the actual Nintendo handles these controllers. The NES system
scans the controllers 60 times per second (every 16.6 ms). The data is buffered and held in
a register that holds the present state of the controller. The rate at which this register is
scanned is controlled by the ROM (usually in a periodic interrupt). Having said this, lets
go back to our example. How often should the controller be scanned? Does it have to be
scanned at exactly the same rate as the NES would do it? No... We are free to scan it
however fast or slow we want. It really doesn't matter! Ideally, we should scan it at LEAST
every 16.6 ms but that is a long time in the world of microcontrollers. I have updated the code below
to take advantage of a couple other features of the PIC. Instead of bit banging the waveforms in
the main loop, I moved the code to an interrupt. This will be very easy to include when we start to add
USB functionality. The scanning code is now implemented in a 17 state finite state machine (FSM).
[Full Source Code]
|
#int_timer1
void NES_Read()
{
static int8 PS = 0;
static int8 FIFO;
set_timer1(60536); // 1 KHz
if(ps == 0)
{
output_bit(NES_LATCH, 1);
output_bit(NES_CLK, 0);
}
else if(ps == 1)
{
output_bit(NES_LATCH, 0);
FIFO = 0;
shift_right(&FIFO, 1, !input(NES_DATA));
}
else if(ps == 15)
{
shift_right(&FIFO, 1, !input(NES_DATA));
output_bit(NES_CLK, 0);
ConState = FIFO;
ps = 255;
}
else if(bit_test(PS,0)) // Falling edge
{
shift_right(&FIFO, 1, !input(NES_DATA));
output_bit(NES_CLK, 0);
}
else // Rising edge
{
output_bit(NES_CLK, 1);
}
if(++ps > 15)
ps = 0;
} |
|
Before I hop right into the USB code, I am going to briefly try to explain the
major details of the USB protocol so that the code makes a little more sense.
The USB protocol is partitioned into several different layers. The root is always
a device. Under a device is a configuration. A device may have different
configurations, such as powered from the USB port or powered externally. Under
each configuration is an interface. The interface describes how the communiction
will take place between the host and the device. Lastly, there are the endpoints where
all of the data transactions occur. Data transfer falls into the following categories:
Control, Interrupt, Bulk, and Isochronous. Each has benefits over others and is more
appropriate in different situations.
In order to establish communication with the USB, the host needs to know information
about the device. The device sends descriptors for each of the layers listed above
that describe how to communicate with a particular device. Lastly, the issue of power;
USB specifies that a device is garunteed a 100mA (5V) block but can request more blocks.
The request for more blocks of power is solely up to the host controller to grant or deny.
With that out of the way, lets talk about how I plan to communicate with the actual PC.
Most USB items that you have ever purchased required you to install drivers on the computer
before you plugged the device in. However, I am going to take advantage of a little secret
that Microsoft Windows already has a lot of drivers that we can easily communicate with, namely
the Human Interface Driver (HID). This is a generic input driver that will work perfectly for
what we are trying to accomplish.
Now with that mini-USB introduction, lets get back to the hardware. In order to implement
USB, we need a USB transciever. There are two options, use any microcontroller we want with
and external USB chip, or use a microcontroller that already has USB support built-in. Since
I am trying to keep the component count to a minimum as well as minimize the cost, the
microcontroller with USB support is a much better choice. Thankfully for us, Microchip has a
PIC that is perfect for this application, the 18F2455. It is a flash programmable device with
built-in USB 2.0 support.
Without further ado, here is the schematic:
And now, the source code!
|
//////////////////////////
// ENTRY POINT //
//////////////////////////
void main()
{
signed int8 out_data[3];
set_tris_a(0x01); // PortA outputs except for NES_DATA (RA0)
setup_timer_1(T1_INTERNAL); // Scan the NES controller
usb_init(); // Initialize USB subsystem
setup_wdt(WDT_ON);
enable_interrupts(INT_TIMER1); // Timer1 interrupts
enable_interrupts(GLOBAL); // Get the clock pulse rocking
printf("\r\n\r\nUSB NES Controller\r\n");
printf("by Drew Hall (dhall@zero-soft.com\r\n\r\n");
printf("Waiting for enumeration...");
while (TRUE)
{
output_bit(LED0, 0); // Both LED's off
output_bit(LED1, 0);
while(!usb_enumerated())
{
restart_wdt(); // Reset the watchdog
putc('.');
output_toggle(LED0);
delay_ms(250);
}
printf("\r\n\r\n***Enumerated***\r\n"); // We are connected to the PC now
output_bit(LED0, 1);
output_bit(LED1, 1);
while(usb_enumerated())
{
restart_wdt(); // Reset the watchdog
out_data[0] = 0; // Transform the data to match the HID
out_data[1] = 0; // This is done this way to match many of
out_data[2] = 0; // the older games that require the data to
// come in a specified order
if(ConState & A)
out_data[2] |= 0x01;
if(ConState & B)
out_data[2] |= 0x02;
if(ConState & SELECT)
out_data[2] |= 0x04;
if(ConState & START)
out_data[2] |= 0x08;
if(ConState & UP)
{
out_data[2] |= 0x10;
out_data[1] = -127;
}
if(ConState & DOWN)
{
out_data[2] |= 0x20;
out_data[1] = 127;
}
if(ConState & LEFT)
{
out_data[0] = -127;
out_data[2] |= 0x40;
}
if(ConState & RIGHT)
{
out_data[0] = 127;
out_data[2] |= 0x80;
}
usb_put_packet(1, &out_data[0], 3, TOGGLE); // Send the data off
delay_ms(10); // Wait for the next polling event
}
printf("\r\n\r\nDevice Un-configured.\r\n");
}
}
|
|
I will leave the reader with an idea, I have also created a set of wireless USB remotes by simply
including a transmitter in the controller. The main problem with this is that the controller
needs batteries because it can no longer be powered from the USB port. It is a non-trivial task
to fit batteries in the controller because lets face it, there just isn't that much empty space in there.
This also requires two microcontrollers instead of just one, although the one inside the controller can be a
very simple one now (12F or 16F series PIC).
While it is easy enough to use the DIP package of the PIC and just solder all of the wires directly onto
the PIC, I decided to make a circuit board that uses all surface mount parts. The board nicely fits in the
controller and is much more reliable than just soldering the wires onto the chip.
[Board Files]
My intention was to inform you, the reader, as an amatuer hobbyist, could create
your own USB Nintendo controller. However, I have had several requests from people to purchase
different parts of the project because they do not have the hardware or tools required. If you are
interested in purchseing any of the components, send me an email. It is not my intent to become rich
from this project. If I were in it for the money, I never would have given away all of the source code
and the schematics (**cough** RetroZone! **cough**).
[Final Source Code]
[Compiled Code (HEX)]
[Final Board Layout]
|
|