About the Wii's SEEPROM
About the Wii's SEEPROM

Chapter 1: Introduction

The Wii contains a Serial Electrically Erase-able Programmable Read Only Memory (Serial EEPROM aka SEEPROM) chip. It is (or similar to) an Atmel AT93C56 Model chip. A pdf file detailing everything about the SEEPROM can be found HERE. Reading the Manual can be intimidating (plus some basic beginner information is left out of the manual), therefore I will do my best in this thread to simplify it. It will still be a lot of content of digest.

The SEEPROM uses 16 bits for a word. There is a total of 128 (0x80) 16-bit words. The use of 16-bit words causes 'offsets' to be a bit weird when talking about the location of data in the SEEPROM. For example, if one was to conventionally say the location/offset of 0x86, the offset according to the SEEPROM is 0x43. Offsets range from 0x00 to 0x7F.

For the rest of this thread, we will refer to the 16-bit words as "Atmel-words".

Offset's listed on this thread (excluding Chapter 5) are in Atmel-word form!

Chapter 2: Hollywood GPIO's

You can communicate with the SEEPROM via the Wii's GPIO Pins (bits). For sending info to the SEEPROM, you use the GPIO_OUT register (0xCD8000E0), and for receiving info from the SEEPROM, you use the GPIO_IN register (0xCD8000E8).

Using the GPIO_OUT register, you send data to the SEEPROM via the following 3 bits...
CS (Chip Select) = Bit 21
SK (Clock) = Bit 20
DI (Data from Wii to SEEPROM; for sending SEEPROM Commands) = Bit 19

Using the GPIO_IN register, there's one bit to read data given from the SEEPROM.
DO (Data from SEEPROM to Wii; for reading output values after a READ Command, or to check the busy/read signal) = Bit 18

Chapter 3: SEEPROM Commands

To communicate with the SEEPROM, you have to use what are called 'Commands'. These Commands is the only form of communicatation that the SEEPROM can understand. Here is a list of all Commands.


All Commands are sent via the GPIO_OUT Register utilizing the DI bit (Bit 19) only. Obviously, this causes an issue, since we can't send anything more than a binary value. Thus, to send commands, a stream of unique bits are sent via the DI bit of GPIO_OUT. Once the SEEPROM has received the full stream of unique bits, it will start running the Command.

Every Command has a Start Bit aka SB. All Commands have a SB value of 1. After SB is the OpCode, this varies per every Command. Next is the Address x16 and/or Data x16.

The Start Bit basically lets the SEEPROM know, hey a Command is now being sent. The OpCode tells the SEEPROM the type of Command it is. The Address/Data x16 is additional info that may need to be provided, this depends on which Command is being sent.

Certain Commands will make the SEEPROM output some bit information via the GPIO_IN Register; DO bit (bit 18).


EWEN (Erase-Write Enable)

SB = 1
OpCode = 00
Address x16 = 11xxxxxx

x = Don't care values, doesn't matter what they are.

This command MUST be executed first before executing any other command (except for READ). The Hex value of this command can vary due to the last 6 bits being don't care values. But for convenience we will say the Hex value of EWEN is 0x4C0 (all don't care values set to 0).


READ (Read)

SB = 1
OpCode = 10
Address x16 = XXXXXXXX (The SEEPROM's Offset; anything from 0x00 thru 0x7F)

This Command will output a stream of 17 bits of data via the DO bit of the GPIO_IN register. The first bit from the output stream is always a dummy bit (value of 0). Excluding the dummy bit, the READ command will output the Atmel-word contents of the Offset that is supplied by the READ Command.

Hex value for READ command for SEEPROM offset 0x44 = 0x644.


WRITE (Write)

SB = 1
OpCode = 01
Address x16 = XXXXXXXX (The SEEPROM's Offset; anything from 0x00 thru 0xF)
Data x16 = ZZZZZZZZZZZZZZZZ (The full Atmel-word to write at designated offset)

This Command will allow you to write in a new Atmel-word at the designated offset. You can write anywhere on the SEEPROM, but writing at the wrong spot will result in your Wii becoming a new coffee coaster (more info in Chapter 13).

Hex value for WRITE value (0x1234) at SEEPROM offset 0x50 = 0x5501234


ERASE (Erase)

SB = 1
OpCode = 11
Address x16 = XXXXXXXX (The SEEPROM's Offset)

Erase means set every bit of the Atmel-word to '1'. It's like a WRITE Command using a Data x16 value of 0xFFFF. ERASE is quicker and simpler to do than WRITE, thus it may be better to execute an ERASE Command if you no longer need certain data on the SEEPROM.

Just like with the command WRITE, executing ERASE at the wrong spot will result in a perma-brick.

Hex value for ERASE at SEEPROM offset 0x02 = 0x702


EWDS (Erase-Write Disable)

SB = 1
OpCode = 00
Address x16 = 00xxxxxx

x = Don't care values

Whenever you are finished with Erasing/Writing to the SEEPROM, you should always execute this command to prevent further unwanted interaction with the SEEPROM. It's not really necessary as every time power is supplied to the SEEPROM (from the Wii turning on), this command is executed (from Starlet's side) for extra safety.

Hex value for EWDS is 0x400.


ERAL (Erase All)

SB = 1
OpCode = 00
Address x16 = 10xxxxxx

x = Don't care values.

This command sets every bit of the entire SEEPROM to '1'. Thus every Atmel-word on the SEEPROM becomes 0xFFFF. This will brick your Wii, for more info why, read Chapter 13.

Hex value for ERAL is 0x480


WRAL (Write All)

SB = 1
OpCode = 00
Address x16 = 01xxxxxx (x = Don't care values)
Data x16 = ZZZZZZZZZZZZZZZZ (The Atmel-word value that will be written at every SEEPROM offset)

This command will write same the Atmel-word value (designated by Data x16) at every offset of the SEEPROM. This will brick your Wii. To find out why, read Chapter 13.

Hex value for WRAL (with value 0x1234) is 0x4401234.


Fyi: Leaked rvl source uses 0's for the don't care values for the SEEPROM Commands.

Chapter 3: Chip Select & Clocking

Even though (while the Wii is turned on) power is always supplied to the SEERPOM, that doesn't necessarily mean it's "On" per say. The SEEPROM must be "On" in order to receive, digest, and execute any Command. The Chip Select bit in the GPIO_OUT Register is how to turn on/off the SEEPROM. To turn on the SEEPROM, you would simply set the Chip Select (CS) bit high. To turn if off, set it low.

Like all other modern SEEPROM chips, the one on the Wii requires what is called 'clocking' for certain periods before, during, and/or after a Command. To 'clock' the SEEPROM, you would set the Clock (SK) bit of GPIO_OUT high. To 'unclock' the SEEPROM, set the SK bit low.

Chapter 4: Timing

The SEEPROM has certain timing requirements to how a Command is received and executed. These timing requirements must be followed or your Command may not execute correctly or execute at all.

For starters, when you set a bit high (like CS) when it's previous state was low, it's not flipped high instantly. It will take some time and this time is known as 'Setup Time'.

CS Setup Time (tCSS) = 200 Nanosecond Wait
DI Setup Time (tDIS) = 400 Nanosecond Wait

SK (clock, unclock) doesn't have a wait for its setup time.

Once certain bits are high and 'setup', they may need to be kept high (or low) for a certain time. This is known as "Holding".

CS Hold Time (tCSH) = 0
DI Hold Time (tDIH) = 400 Nanosecond Wait
SK High Hold Time (tSKH) = 1000 Nanosecond Wait (1 Microsecond)
SK Low Hold Time (tSKL) = 1000 Nanosecond Wait (1 Microsecond)

Chip Select has an extra requirement where if you bring it low (to bring it high later), it has to be low for a certain time.

CS Minimum Low Time (tCS) = 1000 Nanosecond Wait (Page 6 of the Manual says this is 250ns but the chart on page 4 says the wait can be as high as 1000ns depending on the Voltage. For simplicity, we will just put this as 1000ns)

And finally there is  the Write Cycle Time (tWP). It is a range of time that is takes for the SEEPROM to execute a WRITE, ERASE, WRAL, or ERAL Command.

tWP (MILLIseconds) = 0.1 thru 10 #Typically completes in 3 Milliseconds

Chapter 5. Interrupts

Whenever you are writing data to the SEEPROM, you will want to disable interrupts. It's not required for reading data from the SEEPROM though.

Here's a simple snippet of code to disable interrupts:

mfmsr rX
rlwinm rY, rX, 0, 17, 15
mtmsr rY

Keep rX safe somewhere, you will need it when you restore interrupts. To restore them, all you need to do is write back the original MSR (this assumes there are no unrelated function calls in your code that may have modified the MSR to an unknown state).

mtmsr rX

Chapter 6: Wait Subroutine

As mentioned in Chapter 4, certain timing requirements must be followed. We could have special wait subroutines for each specific amount of wait, but that is silly. It's better just to create a single wait subroutine that waits long enough to cover any circumstance. Thus, we need a wait subroutine of 1000 Nanoseconds. You do not want one for tWP (Write cycle time), that will be checked via a Busy/Read signal and is explained more in Chapter 10.

We could write out a Wait Subroutine by just executing some arbitrary nops in a loop. It takes time for Broadway to execute an instruction, but how much time? Well view the following formulae...
  • Bus_Clock = 243,000,000
  • Timer_Clock = (Bus_Clock / 4000)
  • Ticks to Seconds = [(Ticks) / (Timer_Clock * 1000)]
  • Ticks to Milliseconds = [Ticks / Timer_Clock]
  • Ticks to Microseconds = [(Ticks) * 8 / (Timer_Clock / 125)]
  • Ticks to Nanoseconds = [(Ticks) * 8000 / (Timer_Clock / 125)]
  • Seconds to Ticks = [(Secs) * (Timer_Clock * 1000)]
  • Milliseconds to Ticks = [Msecs * Timer_Clock]
  • Microseconds to Ticks = {[(Usecs * (Timer_Clock / 125)]} / 8
  • Nanoseconds to Tricks = {[(Nsecs * (Timer_Clock / 125)]} / 8000

A thousand nanoseconds is just 1 microsecond. So using the second to last formula of this list, we can convert 1 microsecond to its Tick value.
  • {[(1 * (60,750 / 125)]} / 8
  • 60,750 / 125 = 486
  • 1 * 486 = 486
  • 486 / 8 = 60.75

Result is 1 microsecond = 60.75 ticks. (Thus as a future fyi, 1 tick roughly equals 16.461 nanoseconds)

You could write a series of 61 nops for your Wait Subroutine, or make a basic loop, or implement reading Broadway's Time Base. However, we will use the built in timer from Starlet. Starlet's Timer is located at 0xCD800010. Each 'tick' on Starlet's Timer takes exactly 526.7 Nanoseconds to complete.

We just need 2 ticks and we're good. Plus the Starlet Timer will come in handy if you ever happen to mess with the SEEPROM directly from Starlet itself (IOS).

The timer increments up. To start the timer at any time, you just write null to it. Here's a template of basic Wait Subroutine where it assumes r11 is already 0xCD800000 and certain other registers are safe.

li r0, 0 #Reset Starlet Timer
stw r0, 0x0010 (r11) #Start the Timer

lwz r0, 0x0010 (r11) #Check Timer
cmplwi r0, 2 #Check if 2 'tick's (~1000 nanoseconds) has elapsed
blt- wait_loop

blr #End Wait Subroutine

Chapter 7: Prepping the SEEPROM

Let's look at some of the diagrams in the Atmel manual I linked in Chapter 1. Go to Figure to 3 (top of page 8). We will go over the diagram for EWEN as its the most simple Command.

Now remember in order to send a command, you send a stream of bits thru DI (first bit always being '1'). So in the EWEN diagram, we see the stream of bits via the DI bit of GPIO_OUT. In the diagram we see 1,0,0,1,1,X,...,X. The X's are the don't care values and the '...' is simply showing there are more X's in between just the 2 X's shown in the diagram. The '//'s in the diagram present a time cut or else the diagrams would take longer than the width of the page they are shown in.

So what do you notice in the diagram for when the DI bits are being sent? (in fact you will see this pattern on every Command diagram, not just EWEN).

It's that Chip Select is HIGH for the entire time when the Command is executing! Not only that take a closer look, but not at Chip Select.

You may have noticed that SK always starts off as low when the first bit of '1' is sent in the diagram. So we know two things for certain. Before any command we now conclude that CS must be high and SK must be low. We have some issues though, we don't know what CS is beforehand. Therefore, it needs to be set low first in your source.

We will go ahead and set both CS and SK low in the following example snippet of code. The snippet of code is using r11 as 0xCD800000.

lwz r0, 0x00E0 (r11)
rlwinm r0, r0 0, 22, 19 #Clear bit 20 aka SK, & clear bit 21 aka CS
stw r0, 0x00E0 (r11)
bl wait

We know that CS needs to be high before any Command is sent. That will be done shortly. Notice the 'bl wait' at the source. It's calling our earlier Wait Subroutine we made from the Chapter 6.

What's the sync and eieio for?
Broadway is an out of order execution CPU. Since we are dealing with timing requirements, we want certain instructions to be forced 'in order'. This is done with the usage of sync and eieio. You want to place a sync after any load (that is from GPIO_OUT or GPIO_IN). Then you need an eieio after any GPIO-related store. You might be able to get away without this, but not worth it especially if you are Writing Data to the SEEPROM.

Anyway, CS and SK is low, turn on CS to enable the SEEPROM!

lwz r0, 0x00E0 (r11)
ori r0, r0, 0x0400
stw r0, 0x00E0 (r11)
bl wait

At this point the SEEPROM is 'prepped' and is ready to receive a Command.

Chapter 8: Sending Commands

As mentioned before, Commands are sent via a stream of bits. So we will need a loop of some sort to send the bits one after another. You cannot just send each bit back to back to back, it's not that simple. Remember, we have timing requirements to follow. Not only that, there are clocking requirements.

We will go over sending bits for the EWEN command. Referring back to the EWEN diagram (figure 3; page 8 of manual), you will see that DI was sent high THEN SK was sent high. SK would then be sent back to LOW afterwards. This is because SK MUST be low whenever a Command bit is sent via DI!

Obviously, CS is kept high the entire time for any Command. So basically our loop structure is something like this....
  • Loop:
  • Send bit value via DI
  • Wait #For DI setup time
  • Clock
  • Wait #For SK high hold time
  • Unclock
  • Wait #For SK low hold time
  • Repeat Loop

Because every new DI bit that is sent needs the SEEPROM in the unclocked state, we are required to unclock the SEEPROM after clocking it.

Chapter 9: Reading

Reading data isn't too tough. Requires clocking as well. Data from the READ is available once the SEEPROM has received all your bits from the READ command. Basically once you have clocked and unclocked (in your sending bits subroutine), the data is available.

The issue we will run into is the Dummy bit. A bit value of 0 is always sent out first. On page 6 of the Atmel manual for the READ command it says this....

"Output data changes are synchronized with the rising edges of serial clock SK."

What do this mean? It means that in order to receive the next bit of the data, the SEEPROM must receive a clock (SK high). So all we have to do is clock-unclock another extra time, therefore the dummy bit won't be read.

So here's the structure for reading data from the SEEPROM
  • Clock #To get rid of dummy bit
  • Wait #For SK high hold time
  • Unclock
  • Wait #For SK low hold time
  • Read DO bit (remember to read DO from GPIO_IN!!!)
  • Store the bit somewhere safe to later compile it to a hex value
  • Repeat til all 16 bits are read

Afterwards, you can then write some source to compile the bits to an Atmel-word in hex form. Or better yet, you could compile the bits during the reading itself. Have the bits compile as you read them 1 by 1.

Chapter 10: Writing/Erasing

Don't forget that for the WRITE Command, you also send the bits that will be written to the SEEPROM. The WRITE Command is the longest (in bits) of any Command.

Whenever you execute a WRITE, ERASE, WRAL, or ERAL command, the SEEPROM needs some time to preform the task. This is the tWP that was briefly introduced to you back in Chapter 4. Lucky for us, the SEEPROM comes with the feature of a Busy/Ready signal.

After you have finished sending the bits for WRITE, ERASE, WRAL, or ERAL, you wanna check this Busy/Ready signal. On page 6 of the Atmel Manual, it says this for any Writing or Erasing type Command....

"The DO pin outputs the Ready/Busy status
of the part if CS is brought high after being kept low for a minimum of 250 ns (tCS). A
logic “1” at pin DO indicates that the selected memory location has been erased and the
part is ready for another instruction."

Its straight forward. We need to bring CS low, then back to high. Then we can finally check the busy/read signal. Once the signal is good (value is 1), we can do another Command.

Here's a template for checking the Busy/Ready signal~
  • CS low
  • Wait
  • CS high
  • Wait #Because of CS setup time, this is needed!
  • Loop:
  • Check busy/ready signal
  • Repeat Loop

The busy/ready signal is available on the DO bit of GPIO_IN!!!
0 = Busy
1 = Ready

Chapter 11: Cleaning up the Environment

After any Writing or Erasing type of Command, you want to execute a EWDS command to put the SEEPROM into an read-only state for protection.

Also, once you have done that, be sure you bring both CS and SK low, and do one more final wait. Afterwards you're good to restore interrupts (if it was required) and be on your marry way.

Chapter 12: Structure/Map of SEEPROM

NOTE: The offset values listed in this chapter are of the conventional kind. Divide offset values in half for use in any SEEPROM code execution.

The first 72 (conventional offset 0x0 thru 0x47) bytes of the SEEPROM is deemed 'non-writable' according to the leaked RVL source. It is written to once at the factory with the following contents. Therefore, executing a WRAL or ERAL Command on the SEEPROM will not work (well it technically in theory will work, but you will Brick your Wii).

Some of the following may not be 100% accurate (from what I've seen on various Wii SEEPROMs), this is just taken from the leaked RVL source via one document.

Offset 0x0 - MFR_ID; Size 4 bytes
Offset 0x4 - CA_ID; Size 4 bytes
Offset 0x8 - MFR_DATA; Size 4 bytes
Offset 0xC - MFR_SIGN; Size 60 bytes

Now onto the 'writable' stuff...

Offset 0x48 - Boot 2 Version Info; size 20 bytes - WiiCrusher clears words at offsets 0x48 thru 0x5A; I have no idea wth 'WiiCrusher' is
Offset 0x5C - File System Version info; size 18 bytes
Offset 0x6E - Nintendo lot number; size 4 bytes
Offset 0x72 - Nintendo Wii crusher count; size 2 bytes
Offset 0x74 - Key 2; size 16 bytes (this is where the Korean Key is stored; literally called "Key 2" in leaked rvl source)
Offset 0x84 - Hollywood SS Setting; size 2 bytes
Offset 0x86 - Unused, free to write to; size 114 bytes (57 words of free space, remember words are 16-bits)
Offset 0xF8 - RNG; size 8 bytes (final 8 bytes of the SEEPROM) this value is incremented every time IOS starts. The lower 4 bytes of the value is listed first, then the higher 4 bytes.

Chapter 13: About ERAL and WRAL

I decided to write two HBC apps. 1st app, would execute a ERAL then immediately return to HBC, the 2nd app would execute a WRAL (random 16-bit value) then return back to HBC. These apps were made and tested (ERAL tested on one Wii, WRAL on another) before the RVL source was ever leaked. Thus, at the time, I was unaware of the 'unwriteable' region of the SEEPROM.

Anyway, both apps resulted in both Wiis halting when the app was launched and would not return back to HBC. Needless to say, after force shutdown, the Wiis would not boot after powering them on. Don't worry these Wiis were crappy Bootmii IOS Wiis, no real losses occurred, lol.

According the leaked RVL source, the Wii is capable of running the ERAL and WRAL Commands (proper voltage from the VCC pin) but I couldn't find any info as to why the 'unwriteable' region was physically 'unwriteable' unless the SEEPROM chip the Wii uses is custom made and unique.

The only true way to know is to use something as the ERASE Command, the execute a loop of the ERASE Command on the entire SEEPROM (to mimic ERAL). Make an app of this and have the app return back to HBC automatically. If you return back to HBC without a halt, then it's a mystery as why ERAL (or WRAL) won't work..

If the app halts, then we know that the 'unwriteable' region is physically unwriteable to some degree to where attempting writes on it, damages the SEEPROM physically. I would do all this testing myself, but all the Wiis I own now are Boot2 Wiis, and I don't wanna risk losing those.

Chapter 14: Example Sources

Some developers have already made C code to interact with the SEEPROM. Seeprom_write and Seeprom_read have already been made.

However, nobody has written any source code to utilize the SEEPROM's ERASE Command. I thus went ahead and made and wrote my own source code to do so. It's useless as writing is much better obviously. The Erase source is licensed under GPLv2, notice of the license in included in the source.

seeprom_erase - https://mariokartwii.com/downloads/seepr...om_erase.s

Also I wrote up sources for WRAL and ERAL, but they are essentially useless. Both licensed under GPLv2.

seeprom_wral - https://mariokartwii.com/downloads/seepr...rom_wral.s
seeprom_eral - https://mariokartwii.com/downloads/seepr...rom_eral.s

Chapter 15: Seeprom Viewer HBC App

Here's an app I made that displays all your SEEPROM contents on the TV screen - https://github.com/VegaASM/Seeprom-Viewer

Welp, that's it. Thank you for taking the time to read this all.

Forum Jump:

Users browsing this thread: 2 Guest(s)