First of all, you'll need to build yourself an adapter in order to connect the microcontroller to the ROM. For the sake of cleaner code, logically contiguous pins on the ROM, such as A0-A22, D0-D7, should be connected to logically contiguous pins on the microcontroller (an 8-bit microcontroller will only have 8-bit ports, so a good compromise is to utilize full ports as much as possible, e.g. A0->PORTX0, A1->PORTX1 . . . A7->PORTX7, A8->PORTY0, A9->PORTY1, etc.). For my Teensy++ adapter, it looked like this:
(I'm trying to figure out how to create a custom part in Fritzing so I can post a nice image representation of this circuit, but for now you'll have to live with a pin table)
ROM Teensy
Vcc Vcc
Gnd Gnd
A0-7 D0-7
A8-15 C0-7
A16-22 F0-6
D0-7 B0-7
/CE E7
/OE E6
/WE E1
Ok, so now that we have everything wired up (or, in my case, I created a socketed PCB), we can start writing code. The most basic functions are reading and writing a single byte. The function prototypes will look something like this.
uint8_t ReadByte(uint32_t address);
void WriteByte(uint32_t address, uint8_t value);
In order to understand how to implement these functions, we first need to look at the function waveforms in the datasheet. Here's the read function:
We can ignore the actual timings right now, all we really care about is the sequence. From the diagram, we can see that the sequence goes like this:
Set CE# high, OE# high, and WE# high (in any order, or simultaneously)
Set up the address
Set CE# low
Set OE# low
Wait for a short time
Read the data
Set CE# high and OE# high (in any order, or simultaneously)
In code, it looks like this:
uint8_t ReadByte(uint32_t address)
{
// Set data line as input, pulled high
DATA_DDR = 0x00;
DATA_PORT = 0xFF;
// Pull all control lines high
CS_PORT |= CS_BIT;
OE_PORT |= OE_BIT;
WE_PORT |= WE_BIT;
// Set up address
ADDR_PORT_0 = address & 0xFF;
ADDR_PORT_1 = (address >> 8) & 0xFF;
ADDR_PORT_2 = ((address >> 16) & 0x7F);
// Pull CS low, then OE low
CS_PORT &= ~CS_BIT;
OE_PORT &= ~OE_BIT;
delayMicroseconds(1);
// Read data
uint8_t data = DATA_PIN;
// Pull all control lines high
OE_PORT |= OE_BIT;
CS_PORT |= CS_BIT;
return data;
}
As you can see, I've #defined a few values here to make the code cleaner. That's all done based on the pinout specified above. For instance:
#define DATA_PORT PORTB
#define DATA_PIN PINB
#define DATA_DDR DDRB
#define CS_PORT PORTE
#define CS_DDR DDRE
#define CS_BIT (1<<7)
...and so on. Writing is almost identical, though you wouldn't think it, looking at the waveform in the datasheet.
The reason that this looks so complicated is that Flash ROMs actually require you to write several command bytes for every byte of data you actually want to program. However, we want to first write the code to write a single byte, then it's easy to write multiple bytes in a row by making consecutive calls to that function. To make it easier, the sequence is:
Set CE# high, OE# high, and WE# high (in any order, or simultaneously)
Set up the address
Set CE# low
Set WE# low
Set up the data
Wait for a short time
Set CE# high and WE# high (in any order, or simultaneously)
The write operation actually occurs when CE# or WE# is pulled high, which latches the data lines and then performs the write.
No code this time, it should be trivial. Copy and paste the read function and make the necessary changes.
Next, we want to be able to program information to the chip. As mentioned before, this is done by writing several command bytes, followed by the actual data byte. This varies from chip to chip, but for the AM29F032B, the sequence is:
Addr Data
0x555 0xAA
0x2AA 0x55
0x555 0xA0
addr data
where the final "addr" and "data" are the actual address and data that you want to program on the chip. All you have to do is call your WriteByte 4 times in a row with those addresses and data values.
Now, the final function we need to write is to erase the chip. EEPROMs, including Flash ROMs, must be erased before they can be written to. This is because the program function can only change a 1 to a 0, it can't change a 0 to a 1. Because of that, in order to change a 0 to a 1, you have to change EVERYTHING to 1's by erasing the chip, then you can go back and program the 0's. It's just how it is. Anyway, erasing a Flash ROM is achieved by a command sequence. Again, this varies from chip to chip, but for the AM29F032B, the sequence is:
Addr Data
0x555 0xAA
0x2AA 0x55
0x555 0x80
0x555 0xAA
0x2AA 0x55
0x555 0x10
One last thing is that we need to know when the erase function has completed. There are several ways to do so, as described in the data sheet. The lazy way to do it is to continuously read any address (I usually pick 0x000) until the data returned is 0xFF. The reason for this is that during an erase procedure, the result of any read, instead of being the data at that address, is actually a status register. The status register will never equal 0xFF, but once the chip is erased, the whole chip will be all 1's, so any read should return 0xFF. Like I said, it's the lazy way to do it.
So now, we have 4 functions that pretty much handle everything that we need in order to burn code to our Flash ROM:
uint8_t ReadByte(uint32_t address);
void WriteByte(uint32_t address, uint8_t value);
void ProgramByte(uint32_t address, uint8_t value);
void EraseChip();
Now, you have to figure out how to actually transfer data between the PC and the microcontroller. I use RealTerm, because it has the ability to transmit binary files over a serial connection. I then set up my Teensy++ main loop with a simple serial interface that resembles a command-line application, with various commands and flags, then I use RealTerm's send function for programming, and its capture function for reading. Once I've programmed the ROM, I read it back to a file, and compare the file against the original ROM file to make sure that they match (be sure you've padded your ROM file or else trim the file you read back to match the original file size or you may get an incorrect mis-match). Because I'm using the Teensy++'s CDC virtual-serial-port-over-USB interface, it would be entirely possible to write a full PC-side host application tailor-made for this device, but there really isn't much point, seeing as all it would be doing is sending a file to a serial port, or capturing data from that serial port. Better to just use an existing application, if it fits our needs, and RealTerm does just that.
[Minor Edit]
RealTerm apparently does a really terrible job of packet utilization for USB-CDC virtual serial devices, an issue which I've submitted to their bug tracker, though it hasn't generated any response, so it's unlikely to be fixed. For that reason, I've switched to Tera Term, as it speeds up write speeds by a factor of about 20-30, which makes the difference between taking 45 minutes to burn a chip with RealTerm vs about 90 seconds with Tera Term. This doesn't change the fact that I'm still using the same code on the microcontroller side.
Anyway, I'll probably throw up some more pictures at some point, but for the most part, I wanted to describe the process, rather than just handing out schematics and code. This is a relatively simple feat to accomplish, and from what I've seen of a lot of the SNES reproduction makers, I feel it should be something of a rite of passage. If you want to just go out and buy yourself a Willem, go ahead. But be warned that nobody really wants to help repro makers with their crappy Willems. Be a man, roll your own burner.
Here's mine:
An original SNES MaskROM, used for my initial read-only testing |
The double-sided PCB design cut down on PCB size, and as a result, cost |