A complete device takeover & first-ever ROM dump using infrared, with no prior knowledge of the code it runs
This is a rather long article. This article is divided into a few sections. An overview of the hardware, a very cool remote attack on undocumented hardware with many gory details, and then a lot of reverse-engineered pokemon-specific data that would be interesting to romhackers. If you wish to, feel free to skip to The Technical Section, Actual Exploit, ROM Analysis, A Better Exploit, Pokémon Game-Specific Info, or Downloads.
PokéWalker came out in 2009, alongside Pokémon HeartGold/SoulSilver. It was meant to encourage kids who play it to be more active. The basic idea is that as a companion to the game it would reward the player for walking, and thus encourage more walking. Curiously, at the time it came out, a study deemed the PokéWalker one of the best pedometers available at the time. The PokéWalker communicates with the game over infra-red. Since Nintendo DS had no infra-red hardware, the actual game cartridge contained an IR transceiver. This is actually interesting because counterfeit cartridges do not. This means that most Pokémon HeartGold/SoulSilver games on eBay will not work with a PokéWalker. Luckily most sellers are upfront about this.
So, how does a PokéWalker actually work in its natural habitat? It counts steps, and for every step it awards the user an energy unit called "watt". Watts can be traded for mini-games on the walker itself, or sent back to the Nitendo DS game to be used to unlock things there. There is a concept of a "route" that you are walking on, with two unlocked up front, and further routes unlocked as more watts are earned. Depending on the route, one may find different rare Pokémon or items. Items and Pokémon found are influenced by the number of steps taken as well. The game contains 27 routes, of which 20 are unlockable by watts, and 7 are unlockable using special events. One may also upload a Pokémon to the walker to "take it on a stroll". With enough steps taken, the Pokémon may grow a level. A PokéWalker can also interact with another PokéWalker! If two are within range and communication is requested by the user, they will allow the Pokémon in each to "play" with the other. An animation will be shown on each screen, and a random item will be gifted to each player. Team data about the other peer will be saved and later transferred to the DS game, and thus the player may then be battled in the "Trainer House".
The user-interface is a 96x64 2-bit greyscale screen. There are three buttons on the front for user interaction, which function, more or less, as "left", "right", and "center". The interface can be broken up in to a few separate pieces, each of which I'll call a "micro-app". The micro-apps are: Sleep, Home, Menu, Radar, Dowsing, Comms, Stats, Inventory, and Settings. Sleep is the micro-app that handles low-power sleep. Screen is off and steps are being counted. Exiting this micro-app is accomplished by holding the ENTER button for a second. Home is the main screen showing a large walking Pokémon animation, current steps, a small image of the current "route" (a cave, a beach, etc), and some tiny icons for the current inventory on the botton of the screen. Pressing any button on this screen will take you to the Menu. Here, one may launch other-micro apps. Radar allows you to hunt for and catch route-available Pokémon, for a 10-watt cost. Dowsing allows you to search for and obtain route-available items, for a 3-watt cost. Comms is the micro-app that handles infra-red communications with other PokéWalker devices, and with the Pokémon HeartGold/SoulSilver game in a Nintendo DS. Stats, Inventory, and Settings are precisely what you'd expect.
The Pokémon HeartGold/SoulSilver games support many languages, including some asian ones, which are complex to render. The makers of the PokéWalker came up with a very clever solution to rendering text on the device, while having to support to many complex languages: DON'T. Every possible textual string that might need to be shown is converted to an image and uploaded to the device's EEPROM. That way anytime anything needs to be said, it is as simple as showing an image. Thus, when a PokéWalker is bound to a game, all the message images are sent to it in the proper language. There are a lot of them.
Pokéwalker has been on the radar of the Pokémon hacking community for a while, but despite having been released a decade later, no progress had been made on getting a ROM dump.
Minimal documentation existed on the comms protocol (sadly, I found it only after I had figured it all out myself), but it was incomplete and at times wrong.
The EEPROM data had also been somewhat interpreted (sadly, I found it only after I had figured it all out myself), but, also, incomplete and at times very wrong.
In fact, not even all the hardware had been identified.
Identifying the hardware was, of course, the first step to hacking the device. Based on the logo and the markings, the EEPROM was the first to be identified as ST M95512 64KB SPI EEPROM. The CPU was next. It is a member of the Renesas H8/38602R family. Specifically, the rarely-documented H8/38606. The accelerometer is the Bosch BMA150. The IR transceiver was a generic SIR-compatible thing with no distinguishing characteristics whatsoever. The display was harder to identify, and I was not able to identify it until much later in this story. The EEPROM chip is used to store data and can be read out easily using SPI.
The CPU is an H8/300 advanced derivative. I had never before worked with this sort of CPU, but after you've worked in assembly for 100 CPUs, the 101st is pretty simple to get a grasp of. This one was interesting a few ways. To start with, I am not even sure what to call it. It is a mix of 8, 16, 24, and 32-bit architectures. The H8/300 CPU can do math and logic ops on 8, 16, and 32-bit quantities. It can divide and multiply up to 16-bit quantities, and pointers are 24-bits long. The actual SoC here, the H8/38602R is in "normal" mode, which means that it ignores the top 8 bits of any address, and thus has a 64KB address space. Address space is unified, containing both code and data. There are 8 32-bit registers er0 - er7(which is usually the stack pointer). Each 32-bit register erX can be accessed as two 16-bit registers: eX(hi 16 bits) and rX(lo 16 bits). Each of the rX 16-bit registers can be accessed as two 8-bit registers rXl(lo 8) and rXh (hi 8). The memory map is as follows: 0x0000 - 0xBFFF - ROM, 0xF020 - 0xF0FF - MMIO, 0xF780 - 0xFF7F - RAM, 0xFF80 - 0xFFFF - MMIO. Traditionally a full, descending stack is used. CPU is big-endian. In-circuit debugging is supported, but chip will self-erase if programmed, so no way to dump it that way.
The goal was to hack the device without resorting to extreme measures. The only way it communicates is over IR, so I had to sort out how that works. Capturing the pulses using an IR transceiver showed pulses consistent with SIR signaling at 115,200, 8n1. This seemed logical and I went with it. I used an STM32F429 dev board with an IR transceiver to capture some data. The data sent back and forth often had a lot of 0x55 and 0xAA bytes. Generally, you'd expect 0x00 and 0xFF more. Repetition suggests that a simple XOR operation is being used. After some analysis, I concluded that every byte sent was simply being XORed with 0xAA before being sent. Performing this operation on the captured data produced traces that made sense. A few more hours revealed the skeleton of the lowest-level protocol.
Each packet is preceded by an 8-byte header composed of a one-byte command, a one-byte "extra" data, two bytes of checksum, and four bytes of session id, for a total of eight. The checksum took a little bit to work out, but after capturing a number of game-to-walker exchanges I came up with the protocol. Later it turned out that I was almost, but not perfectly correct, but it was close enough. The checksum is calculated over the entire packet and its payload, as if the checksum bytes themselves contained zeroes. First, all the even-numbered bytes are added up, then the odd-numbered ones. The even sum is multiplied by 256 and added to the odd-sum. The top sixteen bits of the sum are then added to the bottom ones, until there are no more set bits in the top 16 bits. Verification is performed the same way: set checksum to zero, calculate proper checksum, check against received one. See 0x0714:calcPacketCrc. The walker advertises itself by sending the 0xFC byte every few hundred milliseconds. This is the only non-packet thing sent.
Some more observation revealed a higher-level protocol. The commands are sent from the DS and replies from the walker. When two walkers talk, one takes the role of the master and one of slave. Almost all commands from the master will have the "extra" byte set to 0x01, replies will almost always have it set to 0x02. After seeing a 0xFC byte advertisement, the master will send a packet of type 0xFA. Session ID will be a random 32-bit value which holds no meaning. Slave will reply with a 0xF8 packet, and Session ID will be another random value, this one generated by the slave. The XOR of the two values becomes the session ID, and for the rest of the communication, all packets not containing this Session ID will be ignored by both sides.
There were a lot of commands used to start and stop a "stroll" and for peer-play between two walkers, but all the scenarios were heavy in what could only be EEPROM read/write commands (since their contents matched up what I would later find in the EEPROM chip). It became clear that commands 0x02 and 0x82 were used to write their payloads to EEPROM directly. They use the "extra" byte as the high byte of the address, and the top bit of the command itself as the 7th bit of the address. Their payloads were always 128 bytes and addresses always 128-byte aligned. The proper reply is an empty packet with type set to 0x04 and "extra" byte echoed from the master's command. Running some tests confirmed all of this. Sending less data than expected still works, and writes whatever was left over from other commands in the buffer. Command 0x0C seemed to be the read command. Its payload is always 3 bytes. First, a 16-bit big-endian address, then an 8-bit length. Lengths over 128 bytes are ignored. The proper reply is a 0x0E packet, with the requested data as contents. This was also easily confirmed.
Packets 0x00 and 0x80 seemed to also be EEPROM writes, but a bit more convoluted. It always began with a 0x10 byte, then 0x80, 0x00, 0x00, and then data that seemed reminiscent of EEPROM data that would then be written. 128 bytes were always written, but less than 128 were sent, so it must be some sort of a compression. It did not take much guestimating to sort out and verify that this is just a form of simple back-referencing compression. This sort of compression is used in a few DS games and in a lot of other places. It is a variant of LZ (Data is compressed by referencing previous decompressed data sort of like "go back 100 bytes in the decompressed data and copy 80 bytes to output now". This works well for data with repetitive pieces.) The compressed data is packetized. Each packet begins with a byte-sized header, followed by 8 chunks. The header is parsed high bit to low bit. If a bit is high, the chunk that follows is a 2-byte backreference. If it is low, the chunk that follows is a single byte to copy to the output. Each backreference is composed of two bytes. The top nibble of the first byte is the copy length minus 3. The bottom nibble, and the next byte make up a 12-bit backreference, minus one. Thus, this protocol can reference at most 4K backwards, and copy up to 18 bytes. Same as for 0x02 and 0x82 commands, sending too little data causes garbage to be written, up to 128 bytes. Looking at some data online, the header bytes are: one byte compression type (0x10) and then a 3-byte little-endian decompressed length.
Generally, when attacking some device, one first considers what attack vectors are available. A piece advice I can give to any aspiring learner of this dark art is to assume that any code you encounter was written by a drunk student. Imagine what mistakes such a person would make, and then go prodding and see if you find them. This method has yet to fail me.
Since I started out knowing nothing about the code running in the device, the first order of business was to see if I can get at least some intel on how the thing works inside. Dumping the complete 64KB EEPROM over IR provided no further info than dumping it over SPI, and it was clear that it contained no code or useful hints. The compressed data format, however, was a possible vector for data exfiltration. Consider that a drunk student would likely not range-check backreferences. Let's try it? Let's send a compressed write made up of 10 80 00 00 ff d0 7f d0 7f d0 7f d0 7f d0 7f d0 7f d0 7f d0 7f. This decompresses to precisely 128 bytes, but all of it is a backreference 128 bytes back, thus leaking to us the 128 bytes preceding the decompression buffer. We then read back the EEPROM address this write command targeted...and....we see things. Looks like the range checks indeed do not exist. Some more exploration yields, however, some sad news. It looks like while the encoding suggests backreferences up to 4K possible, it seems that only 256 bytes of backreference is actually working. Looks like the bottom nibble of the first byte is ignored. That actually makes sense for a decompressor targetting at most 128 bytes of decompressed payload, as this one is. Well, it is a start anyways. So, what do we learn?
Leaking 256 bytes before the decompression buffer shows that 0x80 bytes before it is the payload from the command we last sent (compressed data). Preceding that (at -0x88), are the 8 bytes of the command itself. At 0x9C bytes beforehand, there is the current Session ID. There is not much else to be seen here in this leak. This tells us little, but little is better than none. At least we now know that the decompression buffer is at least 256 bytes into RAM. Given that we have only 2KB of RAM, this is actually useful information.
Next order of business was to try overflowing something, to see what would happen. Ideally we'd see a crash. I tried sending valid and invalid packets of various lengths with valid and invalid CRCs. This avenue was secured. Commands that were too long (over 128 bytes of payload) were ignored. Commands that were too short were accepted, and garbage was used as data. Thus it seems that while they check for too much data, there are no checks for too little. Oops... Sadly this is not exploitable as far as I could tell.
Well, what about compressed data? Can we, perhaps, overflow the decompression buffer? I took about a day to test a few theories about what matters and where. A few things became clear. The first byte, indicating compression type is ignored. It can be anything. While usually the next three bytes indicate decompressed length, the PokéWalker only uses the first byte (limiting decompressed length to 255 bytes), the other two bytes are ignored and can contain anything. Appending more data to a valid compressed buffer also produced no crashes. Tweaking "decompressed length" byte did produce some interesting results, however. But, more about that later.
Since we're allowed to upload just over 120 compressed bytes, that means that, in theory, it can decompress to a lot more data. What if we try to mount a buffer overflow attack using this? First step would be to see if we can trigger a crash. If we successfully overwrite any address with 0xFFFF, we'll crash since this CPU does not like executing from odd addresses, and 0xFFFF has no executable code. First let's figure out how much compressed data we can send. We already know that packet payloads are limited to 128 bytes. We know the compressed data has a 4-byte header, leaving us 124 bytes to play with. Running some tests reveals something interesting. If the "compressed" data is precisely 128 bytes, the PokéWalker treats it as uncompressed and writes it to EEPROM directly. So, really we have 123 bytes to play with. How much can this expand? Let's take a look. Every 8 chunks are preceded by a byte, and every chunk can at most encode 18 bytes (maximum backreference length). Thus we can at most produce 18 * 8 = 144 bytes from every 1 + 2 * 8 = 17 bytes of input. This is not very good compression. The maximal possible compression radio is just under 8.5x. This ratio goes down if you try to actually encode something other than one byte repeating forever. But I digress....
Let's try to spam as many 0xFF bytes as we can. How much is that? Well, first we'll need to emit a single 0xFF bytes, and then we'll use a backreference of one and maximum length to repeat it. We then continue with the backreferences for as long as we can. what does this look like? 7f ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 ff f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 f0 00 c0 f0 00 f0 ff. These 123 bytes will decompress to 1028 bytes of 0xFF. This is the best we can do. Let's send it....nothing. I tried a few variations more, and still nothing. No crashes. Sad, but that is also useful information. This means that there is likely a lot of space after the decompression buffer. Since the SCI3 unit (used for IR comms) config regs are directly after RAM and IR remains functional, we also know that we did not overflow that far, so we know the decompression buffer starts at least 1KB before the end of RAM. Useful info. Now we know it starts at least 256 bytes into RAM, and ends at least 1028 bytes before the end of RAM.
OK, so with a proper compressed payload we cannot overflow anything interesting. Can we do more? Earlier I noticed that tweaking the "decompressed length" produced interesting results. Some more experimentation ensued. The really fun payload was this one 10 0a 00 00 01 aa bb cc dd ee ff 11 00 06 00 ab cd ef 12 23 56 78 9a. This compressed payload should, and does, decompress to these ten bytes: aa bb cc dd ee ff 11 aa bb cc. The rest of the compressed payload is ignored. If we change it to 10 0b 00 00 01 aa bb cc dd ee ff 11 00 06 00 ab cd ef 12 23 56 78 9a (length increased by one), as expected, we see one more byte extracted: aa bb cc dd ee ff 11 aa bb cc ab. Now, if we change it to 10 09 00 00 01 aa bb cc dd ee ff 11 00 06 00 ab cd ef 12 23 56 78 9a(length decreased by one), we see something much more interesting: aa bb cc dd ee ff 11 aa bb cc ab cd ef 12 23 56 78 9a .....(lots of garbage data afterwards). So, it looks like we found a fun bug. It seems that while decompressing LZ backreferences, size checks are only performed at the end, and they are performed for equality and not "greater than or equal". A few more carefully crafted tests confirm this. This is very interesting. Basically, if we do this, the decompressor will keep decompressing. But for how long? A few tests later and ... well well well... It looks like the decompression length is tracked by a 8-bit value, and if we overflow, we get to take another loop around, decompressing until we decompress precisely that many bytes. Now THIS is interesting...
OK, so we know that we can decompress more than 123 bytes, if we could upload them somehow. We'd need to be careful such that we do not accidentally hit the "correct" "decompressed size" accidentally too early, but this is resolvable. At this point I wrote a PC-side tool to simulate decompression of this data to play with these variables. What else do we know? Well, we know that our compressed payload is precisely 128 bytes in ram before the decompression buffer. Given what we've seen so far, it is likely that no further bounds checks exist. But we are still limited to uploading 123 bytes of compressed data, as per protocol limits.
Hey, there's an idea. Since the decompressed buffer follows the compressed buffer, and we can run off the end of the compressed buffer, perhaps if our decompressed data is also valid compressed data, and can be decompressed again, we can extend our reach and actually overflow something useful? This actually took a lot of hand-crafting of the precise bytes to send, but it is possible to do. The practical upshot of all of this is that if we overwrite at least 0x604 bytes, no ill befalls us, and if we overwrite 0x606, we can cause a crash. These values match our expectations.
As this CPU has a full descending stack, it is not unreasonable to assume it will start at the top of RAM. This is typical for most tiny embedded systems. Since we know nothing to the contrary, let's assume that what we hit is the stack. Why? If there were important globals past the decompression buffer we'd expect to hit one before writing 0x600 (!!) bytes. Most likely what we hit is, in fact, the stack. Stacks usually hold important info, like callee-saved registers and the return address. Let's assume that that is what we found. What can we do about it?
We still do not actually know where in memory our decompression buffer is, but we now have even better boundaries. We now know it starts at least 0x100 bytes from the start of RAM, and it ends at least 0x606 bytes from the end of RAM. Given that it is 128 bytes long (since compressed writes write 128 bytes of EEPROM always), we thus can say that it must start at least at 0xF880, and at most at 0xF97A, so we know the location of the decompression buffer to within +/- 125 bytes. Not bad at all, for starting with no information about the device. But it is too early to congratulate ourselves yet!
The decompression overflow hack can now be extended to upload some shellcode and overwrite the stack value we found with an address of it, hoping that what we found is the pushed return address (which are 16 bits on this CPU). Problem is, we do not really know where our buffer is still. So what do we do? We first decompress a lot of NOP instructions, then our code, then the address. The idea is that then we do not care to be perfectly right about where to jump. Anywhere in this long sequence of NOPs is OK to jump to, as the CPU will go through them and get to our code. So, what we want to emit is { <maximal number of zeroes>, <shell code>, <address> }. What address?? I ended up settling on 0xfa80, but anything around there will work, since we'll produce a LOT of NOPs. Conveniently, NOP for this CPU is just 00 00. Cool. I decided to emit the address a few times, so that I could adjust where to place this payload and be more likely to hit "return address" on the stack. The AA bytes here are placeholders for the shellcode payload. Since it is not compressed, they can be anything with no need to worry. The final working exploit compressed payload, as verified by my PC-side tool, was this:
But what code can we upload? Well, we're executing in the context of the Comms micro-app, so we can probably expect to have the IR hardware (SCI3 unit) configured and ready. We can try to just send some bytes and see what happens. The initial shellcode I tried was this one:
Basically this will wait for a space in the IR transmit buffer, and then send a CC byte. Then do that again. Forever. Well, until the device self reboots, likely due to the watchdog timer firing. Did it work? Yes it did! You can try it yourself by just copying those bytes to the start of the AA area in the above doubly-compressed exploit payload. Just remember that we're sending raw bytes, so if you're used to XORing all received data with 0xAA for decryption, you'll see 0x66 and not 0xCC. Oh well. We can run code! Seriously! Starting from nothing to code execution!
There is not much space for shellcode in the exploit payload, and the watchdog timer is quite fast, so we'll not get much out at once. I did not wish to sort out how to quiet down the watchdog, so I decided to dump in pieces. After all, a little more waiting cannot hurt after ten years. The dump shellcode I wrote was:
Does it work? Yes it does! We can dump out about 22K before the watchdog resets us. No problem. Adjust the start address and go again. It takes only a few minutes to get the complete dump, a few more to do it two more times, and another minute to verify all three dumps match! Success!
One of the first things I did was find the LCD driving code to try to identify the LCD controller. Looking at the commands sent and the data format, I guessed that the LCD is based on the SSD1854 controller. The data formats matched perfectly, and the commands (except for one) did too. So I'm calling it, the LCD controller is very likely the SSD1854. It has a very peculiar data format which the PokéWalker uses for all its images. The screen is split into 8-pixel-tall horizontal stripes. Each stripe is scanned horizontally left-to-right. Each column is scanned vertically top to bottom and represented by two bytes. Each byte represents a bitplane. First byte is the more significant bitplane, second is the less-significant bitplane. LSB = top, MSB = bottom. This format was such a mess to glance at that I wrote a tool that can decode and display it, called DECODEIMG. As I prefer working on the console, it was, of course, ASCII-based :). It takes an offset, a number of bytes, and a width as parameters. The input file is read in from stdin, and the image is produced in the console directly! It can handle any images used in the walker (both in EEPROM and ROM) and the walker-intended images in the Pokémon HeartGold/SoulSilver games. This utility and source are available in the Downloads section below.
The system clock is 3.6864MHz, the accelerometer's chip select is port 9 pin 0, the EEPROM's chip select is port 1 pin 2, LCD's chip select is port 1 pin 0, and the LCD's D/nC pin is port 1 pin 1. Port B pin 0 is the enter key, B2 is the left key, and B4 is the right key.
Now that I had the ROM, it was time to document the commands that exist. Here the command column is in hex and the direction column indicates if this is a command from "W"alker or to "W"alker, from "G"ame or to "G"ame. You'll find some VERY interesting commands here! Unless otherwise specified, commands from master have "extra" byte set to 1, and from slave to 2.
CMD | DIR | NOTES |
---|---|---|
00 | g2w | Compressed EEPROM write. Documented at length above. Writes 128 bytes on a 128-byte boundary. "extra" byte is top 8 bits of address. Bottom 8 bits will always be zero. Reply will be CMD_04 |
02 | w2w, g2w | Direct EEPROM write. Documented at length above. Writes 128 bytes on a 128-byte boundary. "extra" byte is top 8 bits of address. Bottom 8 bits will always be zero. Reply will be CMD_04 |
04 | w2w, w2g | ACK for an EEPROM write (CMD_00, CMD_02, CMD_0A, CMD_80, CMD_82) |
06 | ?2w | Direct internal memory write. "extra" byte is top 8 bits of address. First byte of payload is low byte of address. The rest of the payload will be written to internal memory at that address. Yes, this means RAM, MMIO, etc. Reply will be CMD_06 |
0A | ?2w | Direct EEPROM write, random length. "extra" byte is top 8 bits of address. First byte of payload is low byte of address. The rest of the payload will be written to EEPROM at that address. Reply will be CMD_04 |
0C | w2w, g2w | EEPROM read. Payload is a 16-bit start address, big-endian, followed by a one-byte length. Reply will be CMD_0E |
0E | w2w, w2g | EEPROM read reply. Payload is the data requested by CMD_0C |
10 | w2w | Sent by the master walker in a peer-play scenario at start. Enclosed is 0x68 bytes of data - the "IDENTITY DATA". The expected reply from the peer is CMD_12 |
12 | w2w | Sent by the slave walker in a peer-play scenario in reply to CMD_10. Enclosed is 0x68 bytes of data - the "IDENTITY DATA". This data is explained later. The "master" will proceed to exchange needed data, and then will send CMD_14 |
14 | w2w | Sent by the master walker in a peer-play scenario after all the needed data has been exchanged. Enclosed is 0x34 bytes of data - the "PEER PLAY DATA", "extra" byte byte will be 1. Slave walker replies with CMD_14 as well, same format, "extra" byte byte will be 2. Master will then send CMD_16 |
16 | w2w | Sent by the master walker in a peer-play scenario. No data. Reply is also a CMD_16. Does the entire peer-play UI and awards gift item. |
1C | w2w | Sent by the either walker during peer play when it's detected that it's "played" with this peer too recently. Shows the "Cannot connect to trainer again" error. No data. |
20 | g2w | Requests walker's "IDENTITY DATA". No data attached. Walker is expected to reply with CMD_22 |
22 | w2g | Data is "IDENTITY DATA", 0x68 bytes |
24 | ?? | Ping request. No operation other than reply with CMD_26. |
26 | ?? | Ping reply. Sent in reply to CMD_24 |
2A | g2w, w2g | Data is "UNIQUE ID DATA", used to set RTC. Erases the walker. Differs somehow from CMD_2C. Reply is CMD_2A, 0x28 bytes, "UNIQUE ID DATA" which is now all 0xFF. This command is actually used by the game to reset the walker |
2C | ?2w | Data is "UNIQUE ID DATA", used to set RTC. Erases the walker. Differs somehow from CMD_2A. Reply is CMD_2C, 0x28 bytes, "UNIQUE ID DATA" whic is now all 0xFF |
32 | ?2w | Data is "IDENTITY DATA" that master sends to the slave. From it only RTC is used, if a flag is set. Reply from the walker will be CMD_34 |
34 | w2? | Reply to CMD_32, no data |
36 | ?2w | No data, no reply. Shows "Cannot complete this connection" error |
38 | ?2w | Performs the "walk start" action (same as what CMA_5A does), but without the UI. Does not erase "special event" status, unlike CMD_5A which does. See CMD_5A for details. |
42 | ?2w | Same as CMD_32. Data is "IDENTITY DATA" that master sends to the slave. From it only RTC is used, if a flag is set. Reply from the walker will be CMD_44 |
44 | w2? | Reply to CMD_42, no data |
4E | g2w | No data. Reply is CMD_50. Performs "walk end" action, erases pokemon state, shows "walk end" UI |
50 | w2g | Reply to CMD_4E, no data |
52 | ?2w | Same as CMD_32. Data is "IDENTITY DATA" that master sends to the slave. From it only RTC is used, if a flag is set. Reply from the walker will be CMD_54 |
54 | w2? | Reply to CMD_52, no data |
56 | ?2w | No data, no reply. Shows "Cannot complete this connection" error |
5A | g2w | No data. Reply is CMD_5A. Performs "walk start" action, including copying data to proper places, shows the UI |
60 | ?2w | Same as CMD_32. Data is "IDENTITY DATA" that master sends to the slave. From it only RTC is used, if a flag is set. Reply from the walker will be CMD_62 |
62 | w2? | Reply to CMD_60, no data |
64 | ?2w | No data, no reply. Shows "Cannot complete this connection" error |
66 | ?2w | No data. Set the message to show at connection end to be "Completed". Reply is CMD_68 |
68 | w2? | No data. Reply to CMD_66 |
80 | g2w | Compressed EEPROM write. Documented at length above. Writes 128 bytes on a 128-byte boundary. "extra" byte is top 8 bits of address. Bottom 8 bits will always be 0x80. Reply will be CMD_04 |
82 | w2w, g2w | Direct EEPROM write. Documented at length above. Writes 128 bytes on a 128-byte boundary. "extra" byte is top 8 bits of address. Bottom 8 bits will always be 0x80. Reply will be CMD_04 |
9C | ?2w, w2? | No data. Show "Could not receive..." error. Reply is CMD_9C |
9E | ?2w, w2? | No data. Show "Could not receive..." error. Reply is CMD_9E. Sent by walker in reply to CMD_A0, CMD_A2, CMD_A4, CMD_A6, CMD_A8, CMD_AA, CMD_AC, CMD_AE |
A0 A2 A4 A6 A8 AA AC AE | ?2w, w2? | Use byte 0x5A of the most recently received "IDENTITY DATA" as event index. Check if we've participated. If so, reply with an empty CMD_9E, if not, reply with the same CMD as we received, 17 bytes. First 16 will be "EVENT BITMAP", last will be the event index we just checked for. |
B8 BA BC BE | ?2w | No data. Award the user a stamp. In order they are: heart, spade, diamond, club. Replies are CMD_C8, CMD_CA, CMD_CC, CMD_CE respectively |
C0 | ?2w, w2? | No data. Award the user a "special map" This seems to do nothing but show an icon in the UI, and show the "Special map received" message. Reply is also an empty CMD_C0 |
C2 | ?2w, w2? | No data. Award the user an "event pokemon". Details later. Reply is also an empty CMD_C2 |
C4 | ?2w, w2? | No data. Award the user an "event item". Details later. Reply is also an empty CMD_C4 |
C6 | ?2w, w2? | No data. Move the user to an "event route". Details later. Reply is also an empty CMD_C6 |
C8 CA CC CE | w2? | Replies to CMD_B8, CMD_BA, CMD_BC, CMD_BE respectively. No data. |
D0 | ?2w | No data. Same as CMD_C0, except also grant the user all 4 stamps |
D2 | ?2w | No data. Same as CMD_C2, except also grant the user all 4 stamps |
D4 | ?2w | No data. Same as CMD_C4, except also grant the user all 4 stamps |
D6 | ?2w | No data. Same as CMD_C6, except also grant the user all 4 stamps |
D8 | ?2w | No data. Show "Cannot complete this connection" error. No reply. |
F0 | ?2w, w2? | Not yet fully understood. Data is 0x71 bytes of "ENROLL DATA" Some sort of enroll action. The only path to code that writes te EEPROM's "UNIQUE ID DATA" and "LCD INIT DATA". Reply is also a CMD_F0, with 0x28 bytes of data - a copy of the "UNIQUE ID DATA" |
F4 | ?2w | Immediate disconnect. No reply. |
F8 | w2g, w2w | Part of connection initiation as explained above. No data. Sent by slave walker to master walker or game after getting CMD_FA |
FA | g2w, w2w | Part of connection initiation as explained above. No data. Sent by game or master walker to slave walker after seeing an advertising byte 0xFC on IR |
FE | ?2w, w2? | Not yet fully understood. Writes the 8 data bytes to EEPROM at offset 8. Never seen in the wild. Reply is an empty CMD_FE |
Yes, CMD_06 looks very interesting. It allows direct write to RAM. There is no corresponding read command, but it is still nice to have this primitive. This means that instead of doing the messy stack overflows that I had to do, you can just upload code to unused RAM (which there is a lot of). Jumping to it is not immediately obvious. But looking at the disassembly, it is trivial. You see, the "main event loop" of any micro-app is called in a loop by the main code of the PokéWalker. The pointer is a global, situated at 0xF7E0. This means that arbitrary code execution is as simple as uploading code to anywhere from 0xF956 to 0xFF40, and then writing a big-endian address to call to 0xF7E0 using CMD_06. If you wish to then return gracefully to the OS, just rewrite 0xF7E0 with a pointer to the normal Comms app main loop: 0x08D6. Yup... arbitrary code execution on the pokewalker is that easy. If you intend to do things for a while, it would help to pet the watchdog. Call 0x259E for that occasionally. Oh, and if you want to send IR packets, write your packet payload at 0xF8D6, and call 0x0772. Set r0l to payload length, r0h to the command, r1l to the "extra" byte. Checksum and session ID will be handled for you. Cool, right?
In fact, that brings us to a much better method to dump the ROM - using proper packets and checksums. I use CMD_06 to upload the following shellcode to 0xF956, and then receive 384 128-byte valid checksummed packets. This is the method the PalmOS app I am releasing uses.
In a few places in the ROM, over-cleverness can be easily noted. For example this little tidbit here on the right. Of course you've seen the clever way to swap two values without needing a temporary variable long ago in some trivia. It is useful even today, in very constrained assembly coding. The code here was clearly compiled, and not hand-assembled. And here someone tried to do this swap, except the values are array elements, so the compiler instead generated 3 loads and 3 stores, for what should have been a lot simpler. It is very funny that someone trying to be too clever for themselves things can even be seen in assembly.
Another over-clever thing in the PokéWalker is the function at 0x36F2. It reads a given entry table from EEPROM containing a set of EEPROM regions to checksum. It then checksums the region, and hangs the PokéWalker if the checksum is invalid. I have no idea why this was implemented, but the function is called randomly throughout the code. You'd think it would protect important regions of the EEPROM, but it does not. Technically, that table can be modified by the DS game over IR, but it never is. It is always statically protecting unused EEPROM areas. How useful...
The ROM contains what appears to be factory tests. At boot, it will send the byte 0xB2 on the same SPI chip select as the display. If it gets back an 0xAA, the tests run, else it will send a 0xB0 and skip them. You can see the code for this at 0x5990:factoryTestPerformIfNeeded. First the tests send what I guess is the version number of the ROM. It is read from 0x005C-0x005D. Then the build string is sent - it is 12 bytes at 0x0050 and, for my device, contains "Jun 26 2009", terminated by a zero byte. Then a 0x04 is sent. EEPROM factory test is then performed. If it is failed, 0xF4 is sent and the decice hangs. Then, EEPROM is erased to be in a sane state. After this, a 0x03 is sent. The RTC test is performed now. On failure, a 0xF3 is sent and the decice hangs. Beginning to see a pattern yet? Next, a 0x02 gets sent. Accelerometer test (0xA830:accelFactoryTest) is a bit more complex, and sends some data of its own. Finally, a 0x01 gets sent. The device now expects a precise voltage applied to the battery port. It is measured, redundantly encoded, and saved in the EEPROM. This will be a reference used for the life of the device to determine when the battery is low. On failure, as you could guess, a 0xF1 is dispatched and the device is hung. If all tests pass, a 0x00 goes out. Accelerometer is then shut down and the device goes to sleep, ready to be powered off and sent to the happy consumer.
A few structs of data keep recurring, so I will detail them here all at once. You'll see them referred to by their friendly name, which you'll find here in comments. Keep in mind that any strings are stored in a proprietary 16-bit-wide encoding.
0x0000 - 0x0007 | "nintendo" string as a magic marker. If ROM does not find this at boot, it will consider the walker empty and uninitialized | ||
0x0008 - 0x000F | some value written during personalization. Never read. | ||
0x0010 - 0x0071 | ??? | ||
0x0072 | Number of watchdog resets | ||
0x0073 - 0x007f | ??? | ||
0x0080-0x0082 | factory-provided ADC calibration data. (RELIABLE DATA FORMAT, copy at 0x0180) | ||
0x0083-0x00AB | struct UniqueIdentityData. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x0183) | ||
0x00AC-0x00EC | struct LcdConfigCmds. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x01AC) | ||
0x00ED-0x0155 | struct IdentityData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x01ED) | ||
0x0156-0x016E | struct HealthData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x0256) | ||
0x016F-0x0171 | struct CopyMarker. Used at walk init time (RELIABLE DATA FORMAT, copy at 0x26F) | ||
0x0172-0x017F | unused | ||
0x0180-0x0182 | factory-provided ADC calibration data. (RELIABLE DATA FORMAT, copy at 0x0080) | ||
0x0183-0x01AB | struct UniqueIdentityData. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x0083) | ||
0x01AC-0x01EC | struct LcdConfigCmds. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x00AC) | ||
0x01ED-0x0255 | struct IdentityData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x00ED) | ||
0x0256-0x026E | struct HealthData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x0156) | ||
0x026F-0x0271 | struct CopyMarker. Used at walk init time (RELIABLE DATA FORMAT, copy at 0x16F) | ||
0x0272-0x027F | unused | ||
0x0280-0x041F | Numeric character images: "0123456789:-/", 8x16 each, in this order | ||
0x0420-0x045F | WATT symbol image 16x16 | ||
0x0460-0x046F | pokeball 8x8 | ||
0x0470-0x047F | pokeball light grey 8x8 (used for event pokemon) | ||
0x0480-0x0487 | unused | ||
0x0488-0x0497 | item symbol 8x8 | ||
0x0498-0x04A7 | item symbol light grey 8x8 (used for event items) | ||
0x04A8-0x04B7 | tiny map icon 8x8 (used for "special map" reception) | ||
0x04B8-0x04F7 | card faces: heart, spade, diamond, club, 8x8 each (used for "stamp" reception) | ||
0x04F8-0x05B7 | arrows (up down left right), each in 3 configs (normal, offset, inverted) 8x8 each | ||
0x05B8-0x05D7 | left arrow for menu 8x16 | ||
0x05D8-0x05F7 | right arrow for menu 8x16 | ||
0x05F8-0x0617 | "return" symbol for menu 8x16 | ||
0x0618-0x063F | unused | ||
0x0638-0x0647 | symbol for "have more message" in the bottom right of messages. ORRED into last 8 columns (thus 16 bytes). Applied after 0x0648 | ||
0x0648-0x064F | symbol for "have more messages" in the bottom right of messages. Each byte here is ANDed with each col of last 8 in the message. Both bitplanes (so you can make it black or keep as is). applied before 0x0638 | ||
0x0650-0x065F | medicine vial (?) icon 8x8 | ||
0x0660-0x066F | low battery icon 8x8 | ||
0x0670-0x08AF | large talk bubbles from bottom right with pokemon feeling icon (exclamation, heart, music note, smile, neutral face, ellipsis) 24x16 each, 6 of them | ||
0x08B0-0x090F | large talk bubble from bottom left with exclamation point in it 24x16 | ||
0x0910-0x0A4F | "POKé RADAR" menu heading in a box. 80x16 | ||
0x0A50-0x0B8F | "DOWSING" menu heading in a box. 80x16 | ||
0x0B90-0x0CCF | "CONNECT" menu heading in a box. 80x16 | ||
0x0CD0-0x0E0F | "TRAINER CARD" menu heading in a box. 80x16 | ||
0x0E10-0x0F4F | "POKéMON & ITEMS" menu heading in a box. 80x16 | ||
0x0F50-0x108F | "SETTINGS" menu heading in a box. 80x16 | ||
0x1090-0x10CF | "poke-radar" icon for main menu 16x16 | ||
0x10D0-0x110F | "dowsing" icon for main menu 16x16 | ||
0x1110-0x114F | "connect" icon for main menu 16x16 | ||
0x1150-0x118F | "trainer card" icon for main menu 16x16 | ||
0x1190-0x11CF | "pokemon & items" icon for main menu 16x16 | ||
0x11D0-0x120F | "settings" icon for main menu 16x16 | ||
0x1210-0x124F | "person" icon for trainer card screen 16x16 | ||
0x1250-0x138F | trainer's name rendered as an image 80x16 | ||
0x1390-0x13CF | small route image for "trainer card" screen 16x16 | ||
0x13D0-0x146F | "steps" in frame for second screen of trainer card 40x16 | ||
0x1470-0x14EF | "time" in frame for second screen of trainer card 32x16 | ||
0x14F0-0x158F | "days" in frame for second screen of trainer card 40x16 | ||
0x1590-0x168F | "total days:" in frame for second screen of trainer card 64x16 | ||
0x1690-0x172F | "sound" in frame for preferences screen 40x16 | ||
0x1730-0x17CF | "shade" in frame for preferences screen 40x16 | ||
0x17D0-0x182F | speaker icon with no waves (no sound) for preferences screen 24x16 | ||
0x1830-0x188F | speaker icon with one wave (low sound) for preferences screen 24x16 | ||
0x1890-0x18EF | speaker icon with two waves (high sound) for preferences screen 24x16 | ||
0x18F0-0x190F | contrast demonstrator (drawn a bunch of times over) 8x16 | ||
0x1910-0x19CF | large treasure chest icon for item view 32x24 | ||
0x19D0-0x1A8F | large map scroll thingie 32x24 | ||
0x1A90-0x1B4F | large present icon for item view 32x24 | ||
0x1B50-0x1B8F | small bush dark-colored, for dowsing 16x16 | ||
0x1B90-0x1BCF | small bush light-colored, for dowsing 16x16 | ||
0x1BD0-0x1C4F | "LEFT: "string on white background. seems unreferenced 32x16 | ||
0x1C50-0x1CAF | blank image 16x24 | ||
0x1CB0-0x1C6F | bush dark 32x24 | ||
0x1D70-0x1DAF | word bubble with one exclamation point (for poke hunting) 16x16 | ||
0x1DB0-0x1DEF | word bubble with two exclamation points (for poke hunting) 16x16 | ||
0x1DF0-0x1E20 | word bubble with three exclamation points (for poke hunting) 16x16 | ||
0x1E30-0x1E6F | three lines radiating from bottom left (for bush we just clicked) 16x16 | ||
0x1E70-0x1EEF | skewed small 7-pointed star (attack) 16x32 | ||
0x1EF0-0x1F6F | skewed large 7-pointed star (critical hit attack) 16x32 | ||
0x1F70-0x202F | cloud "for pokemon appeared" 32x24 | ||
0x2030-0x203F | "HP" item (4 of these make up an HP bar) 8x8 | ||
0x2040-0x204F | a little 5-pointed star image for when we catch something 8x8 | ||
0x2050-0x234F | "attack/evade/catch" directions placard for battles 96x32 | ||
0x2350-0x244F | pokewalker image, blank screen, 32x32 | ||
0x2450-0x246F | IR xmit icon (like wifi arcs) 8x16 | ||
0x2470-0x247F | music note icon 8x8 | ||
0x2480-0x248F | blank icon 8x8 | ||
0x2490-0x252F | "HOURS" in a pretty frame. appears unused 40x16 | ||
0x2530-0x26AF | "Connecting..." string for comms 96x16 | ||
0x26B0-0x272F | "No trainer found" string for comms 96x16 | ||
0x2830-0x2B2F | "Cannot complete\nthisConnection" string for comms 96x32 | ||
0x2B30-0x2CAF | "Cannot connect" string for comms 96x16 | ||
0x2CB0-0x2FAF | "Other trainer\nisunavailable" s60tring for comms 96x32 | ||
0x2FB0-0x21AF | "Already received\nthis event" string for comms 96x32 | ||
0x32B0-0x34AF | "Canont connect\nto trainer again" string for comms 96x32 | ||
0x35B0-0x37AF | "Could not\nreceive..." string for comms 96x32 | ||
0x38B0-0x39AF | "has arrived!" string for comms 96x16 | ||
0x3A30-0x3BAF | "has left." string for comms 96x16 | ||
0x3BB0-0x3C2F | "received!" string for comms 96x16 | ||
0x3D30-0x3EAF | "Completed!" string for comms 96x16 | ||
0x3EB0-0x302F | "Special Map" string for comms 96x16 | ||
0x4030-0x41AF | "Stamp" string for comms 96x16 | ||
0x41B0-0x422F | "Special Route" string for comms 96x16 | ||
0x4330-0x44AF | "Need more Watts." string 96x16 | ||
0x44B0-0x462F | "No Pokemon held!" string 96x16 | ||
0x4630-0x47AF | "Nothing held!" string 96x16 | ||
0x47B0-0x492F | "Discover an item!" string 96x16 | ||
0x4930-0x4AAF | "found!" string 90x16 | ||
0x4AB0-0x4C2F | "Nothing found!" string 90x16 | ||
0x4C30-0x4DAF | "It's near!" string 90x16 | ||
0x4DB0-0x4E2F | "It's far away..." string 90x16 | ||
0x4F30-0x40AF | "Find a Pokemon!" string 90x16 | ||
0x50B0-0x522F | "Found something!" string 90x16 | ||
0x5230-0x52AF | "It got away..." string 90x16 | ||
0x53B0-0x552F | "appeared!" string 90x16 | ||
0x5530-0x55AF | "was caught!" string 90x16 | ||
0x56B0-0x572F | "fled..." string 96x16 | ||
0x5830-0x59AF | "was too strong." string 96x16 | ||
0x59B0-0x5B2F | "attached!" string 96x16 | ||
0x5B30-0x5CAF | "evaded!" string 96x16 | ||
0x5CB0-0x5E2F | "A critical hit!" string 96x16 | ||
0x5E30-0x5FAF | " " (yes a bunch of spaces) string 96x16 | ||
0x5FB0-0x512F | "Threw a Poke Ball." string 96x16 | ||
0x6130-0x61AF | "Almost had it!" string 96x16 | ||
0x62B0-0x642F | "Stare down!" string 96x16 | ||
0x6430-0x65AF | "lost!" string 96x16 | ||
0x65B0-0x662F | "has arrived" (for walker to walker) string 96x16 | ||
0x6730-0x68AF | "Had adventures!" string 96x16 | ||
0x68B0-0x6A2F | "Play-battled." string 96x16 | ||
0x6A30-0x6BAF | "Went for a run." string 96x16 | ||
0x6BB0-0x6D2F | "Went for a walk." string 96x16 | ||
0x6D30-0x6EAF | "Played a bit." string 96x16 | ||
0x6EB0-0x602F | "Here's a gift..." string 96x16 | ||
0x7030-0x71AF | "cheered!" string 96x16 | ||
0x71B0-0x732F | "is very happy!" string 96x16 | ||
0x7330-0x74AF | "is having fun!" string 96x16 | ||
0x74B0-0x762F | "is feeling good!" string 96x16 | ||
0x7630-0x77AF | "is happy." string 96x16 | ||
0x77B0-0x792F | "is smiling." string 96x16 | ||
0x7930-0x7AAF | "is cheerful." string 96x16 | ||
0x7AB0-0x7C2F | "is being patient." string 96x16 | ||
0x7C30-0x7DAF | "sits quietly." string 96x16 | ||
0x7DB0-0x7F2F | "turned to look." string 96x16 | ||
0x7F30-0x70AF | "is looking around." string 96x16 | ||
0x80B0-0x822F | "is looking this way." string 96x16 | ||
0x8230-0x83AF | "is daydreaming." string 96x16 | ||
0x83B0-0x852F | "Found something." string 96x16 | ||
0x8530-0x86AF | "What?" string 96x16 | ||
0x86B0-0x882F | "joined you!" string 96x16 | ||
0x8830-0x89AF | "Reward" string 96x16 | ||
0x89B0-0x8B2F | "Good job!" string 96x16 | ||
0x8B30-0x8C6F | "Switch?" string 80x16 | ||
0x8C70-0x8CAF | ??? | ||
0x8CB0-0x8CDF | random checksum area descriptor addrs (see 0x36F2:randomEepromChecksumCheck, struct struct RandomCheckInfo) | ||
0x8CF0-0x8EFF | random garbage data that is checksummed by randomEepromChecksumCheck() | ||
0x8F00-0x8FBD | struct RouteInfo - current route data | ||
0x8FBE-0x907D | current "area" we are strolling in graphic 32x24 | ||
0x907E-0x91BD | current "area" we are strolling in textual name 80x16 | ||
0x91BE-0x933D | current pokemon animated sprite for "held items and pokemon" screen, fights, etc. 32 x 24 x 2 frames | ||
0x933E-0x993D | current pokemon large nimated sprite for main screen 64 x 48 x 2 frames | ||
0x993E-0x9A7D | cur pokemon name image 80x16 | ||
0x9A7E-0x9EFD | route available pokemon selected by the same animated small sprites 32 x 24 x 2 frames x 3 pokemon | ||
0x9EFE-0xA4FD | large animated image (like at 0x933E) but of the third (option C) available pokemon on this route. used for "joined your walk" situation 64 x 48 x 2 frame | ||
0xA4FE-0xA8BD | available pokemon name images 80x16 x3 pokemon | ||
0xA8BE-0xB7BD | item names as images. 96x16 x 10 images (one per item) | ||
0xB7BE-0xB7FF | ??? | ||
0xB800 | Bitfield of special things received. 0x01 - "heart" stamp received, 0x02 - "spade" stamp received, 0x04 - "diamond" stamp received, 0x08 - "club" stamp received, 0x10 - "special map" received, 0x20 - walker contains an event pokemon (gifted or caught), 0x40 - walker contains event item (gifted or dowsed), 0x80 - walker has received a "special route" | ||
0xB801-0xB803 | unused | ||
0xB804-0xBA43 | data for "special map received". Format unknown. Possibly used by the DS games, but no evidence of this found in the games. | ||
0xBA44-0xBA53 | gifted event poke, or radar-caught event poke | basic data. struct PokemonSummary | |
0xBA54-0xBA7F | extra data. struct EventPokeExtraData | ||
0xBA80-0xBBFF | small sprite 32 x 24 x 2 frames | ||
0xBC00-0xBD3F | name image 80x16 | ||
0xBD40-0xBD47 | gifted event item, or dowsed event item | item data. 6 bytes of zeroes, then u16 item, LE | |
0xBD48-0xBEC7 | item name image 96x16 | ||
0xBEC8-0xBEFF | unused | ||
0xBF00-0xBF05 | "special route" info (struct SpecialRoute): | 6 bytes of zeroes (part of item struct but unused by DS or walker) | |
0xBF06 | enum RouteImageIdx | ||
0xBF07 | unused | ||
0xBF08-0xBF17 | special route-available pokemon basic data. struct PokemonSummary | ||
0xBF18-0xBF43 | special route-available pokemon extra data. struct EventPokeExtraData | ||
0xBF44-0xBF45 | min steps to encounter this poke on the route. u16 LE | ||
0xBF46 | percent chance to encounter this poke on route after step minimum met | ||
0xBF47 | unused | ||
0xBF48-0xBF49 | special route-available item. u16 LE | ||
0xBF4A-0xBF4B | min steps dowse this item. u16 LE | ||
0xBF4C | percent chance to dowse this item on route after step minimum met | ||
0xBF4D-0xBF4F | unused | ||
0xBF50-0xBF79 | routeName u16[21] | ||
0xBF7A | "event index" for catching this route's special pokemon | ||
0xBF7B | "event index" for dowsing this route's special item | ||
0xBF7C-0xC6FB | special route pokemon animates small sprite. 32 x 24 x 2 frames. should be 0x180 bytes big, but it 0x170. no idea why but confirmed | ||
0xC6FC-0xC83B | special route pokemon name image 80x16 | ||
0xC83C-0xC8FB | special routes's large image for home screen, like 0x8FBE is for a normal route 32x24 | ||
0xC8FC-0xCA3B | special routes's textual name 80x16 | ||
0xCA3C-0xCBBB | special route item textual name 96x16 | ||
0xCBBC-0xCBFF | unused | ||
0xCC00-0xCE23 | struct TeamData on our whole team, so that any walkers we peer play with transfer it to their DS game and we can be battled in the trainer house | ||
0xCE24-0xCE7F | also written at walk start time as part of the above. probably just to keep the write a multiple of 0x80 bytes | ||
0xCE80-0xCE87 | unused | ||
0xCE88 | If low bit set, game will give player a STARF berry ONCE per savefile. Used when 99999 steps reached | ||
0xCE89 | unused | ||
0xCE8A-0xCE8B | current watts written to eeprom by cmd 0x20 before replying (likely so remote can read them directly). u16 BE | ||
0xCE8C-0xCEBB | 3x route-available pokemon we've caught so far. 3x struct PokemonSummary | ||
0xCEBC-0xCEC7 | 3x route-available items we've dowsed so far. 3x {u16 LE item, u16 LE unused} | ||
0xCEC8-0xCEEF | 10x route-available items we've been gifted by peer play. 3x {u16 LE item, u16 LE unused} | ||
0xCEF0-0xCF0B | historic step count per day. u32 each, BE, [0] is yesterday, [1] is day before, etc... | ||
0xCF0C-0xD47F | Event log. Circularly-written, displayed in time order. 24x struct EventLogItem | ||
0xD480-0xD6FF | team data written here before walk start action. struct TeamData | ||
0xD700-0xDBCB | scenario data written here before walk start action. everything that 0x8F00-0xB7FF would have | ||
0xDC00-0xDE23 | current peer play peer. struct TeamData. uploaded as part of peer play. later shifted to index [0] at 0xDE24 list of peers | ||
0xDE24-0xF38B | Peers we've met. For battle house info. Newest element is first. 10x struct TeamData | ||
0xF38C-0xF3FF | unused | ||
0xF400-0xF57F | peer play temporary data about peer | medium pokemon animated image of pokemon we are peer-playing with (never erased) 32x24 x 2 frames | |
0xF580-0xF6BF | rendered text name of pokemon we are peer-playing with 80x16 | ||
0xF6C0-0xF6F7 | data. struct PeerPlayData |
The PokéWalker has a concept of an "event" - a thing that should only be done once. A few things can be gated on an event by id, and a persistent bitfield stored in the EEPROM's "IDENTITY DATA" stores which events the user has already participated in. There even exist remote commands to query whether a user has participated in a given event by index. Those would be CMD_A0, CMD_A2, CMD_A4, CMD_A6, CMD_A8, CMD_AC, and CMD_AE. A few checks are also done internally to the rom. For example special routes may have a pokemon that can only be caught once, or an item that may only be dowsed once. Event indices are 1...127, allowing for 127 events. If an index of zero is used, the function that checks for whether we've already participated in a given event (0x1D7A:checkForWhetherWeveParticipatedInAGivenEvent) returns false always, and the function that records participation (0x1D22:setThatWeveParticipatedInAGivenEvent) does nothing. Despite much checking, I do not think this related to the event-unlockable routes and is intead something else. I found no evidence of this being used by the game or peer play. Must be something that was planned but never realized. That being said, one can use this by crafting a special route.
The PokéWalker treats some data as very critical and in need of special protection. This type of data is stored in a container I dubbed "RELIABLE DATA". The format is simple: the data is stored in two places in EEPROM, and is followed by a one-byte checksum (sum of all the bytes). When writing, the same data is written to both places, and so it the checksum. When reading both places are read and checksums checked. If only one sum matches, that data is used, and the other location overwritten with this data. If both sums fail, data in both locations is filled with 0xFF. If both checksums match, they are compared. If they match, nothing more is done and data is returned. If they do not match, it means that a previous write was interrupted just between writing the first area and the second, leaving them both valid, but second with older data. In this case second area is overwritten with data from the first.
As you can see above in the EEPROM map, a few instances of "RELIABLE DATA" are used. They contain things like step and watt info, factory calibration for the ADC for more precise battery level measurement, LCD initialization commands, walker's unique identity data, and the "Copy Marker". The copy marker is used for "walk start" action to provide for some atomicity. More on this later
With both walkers sending out 0xFC advertising bytes, it is only a matter of time before one receives the other's and decides to become the master in the connection. It will then send a CMD_FA, to which the now-slave walker will reply with a CMD_F8. With those pleasantries out of the way, the connection is officially established. The master will now send a CMD_10, enclosing its "IDENTITY DATA". Slave will check for version compatibility and whether it can peer play with this peer. If so, it will reply with a CMD_12, enclosing its "IDENTITY DATA". Master will perform the same checks, and if they pass, it will begin some data exchange using EEPROM reads/writes, accessing slave's EEPROM directly. First, the master's small animated walking pokemon sprite (which is at EEPROM:0x91BE) is sent to slave's EEPROM:0xF400. Next the master's Team data (from EEPROM:0xCC00) is sent to the slave's EEPROM:0xDC00. Next, the master will read the slave's small animated walking pokemon sprite from the slave's EEPROM:0x91BE, and write it to its own EEPROM:0xF400. Then, then slave's team data from EEPROM:0xCC00 will be read and written to master's EEPROM:0xDC00. At this point both devices have the peer's data in their EEPROMS.
Now the master sends CMD_14, with its "PEER PLAY DATA", which the slave will write to its EEPROM:0xF6C0. The slave replies with its "PEER PLAY DATA". The master records it at EEPROM:0xF6C0 well. This data will later used to figure out which gift each side will get. Master now sends a CMD_16 with no data, to which the slave responds with the same. IR communication is now over, and the little playful animation about "playing" with the other pokemon is shown on both screens. The animated sprite from the other walker is used for it, as well as our own. It is also flipped horizontally if either pokemon is shiny. Then the gift is calculated and applied. The function that does it is 0x6382:calculateAndApplyPeerGift. It first calculates a seed, which is seed = MAX(20000, 10 * (remote.watts + my.watts) + (remote.steps + my.steps)). It is notable that both walkers will end up with the same seed calculation. Then the walker checks if there is space in the peer-gifted items array, which has space for ten items from peer play (EEPROM:0xCEC8). If the array is full, watts are awarded, according to this equation: watts = MAX(99, seed / 200). If there is a space for an item, some more convoluted comparisons between our and peer's step counts ensue to decide which item to award.
The usual connection establishment happens, then the DS will query the walker for the "IDENTITY DATA" using CMD_20. The walker will reply with CMD_22 and the identity data. The DS will then use CMD_52 to set the walker's RTC. Walker will reply with an empty CMD_54. The DS will then write a LOT of data to walker's EEPROM:0xD700, up until the end. This data is temporarily staged there. The DS will then write some data to the walker's EEPROM:0xD480. 0x224 bytes are written. This is the team information on your current 6-pokemon team. Format is "struct TeamData". This data is temporarily staged there. The DS will then read the walker's EEPROM in case there are items or pokemon to collect. Sidenote: this is the same procedure that is used at walk-end time. The EEPROM ranges read are: EEPROM:0xCE80-EEPROM:0xDBCB and EEPROM:0xB800-EEPROM:0xBEC7. They contain gifted and collected items and pokemon, event-related items and pokemon, the step count log, the event log, and the log of all encountered peers' teams. Then, for some reason, the DS will send a ping (CMD_24). The walker will dutifully reply with a pong (CMD_26). The DS will then send CMD_5A, which starts the walk in the walker. The walker will reply with CMD_5A as well, and the DS will send a CMD_F4 to disconnect.
So, what does the actual walk-start process do in the walker? The code for this is in the 0x009a:performActionAsRequestedByRemote function, and starts at offset 0x00FC. First, 0x048C:startWalkEepromActions is called. Then the byte at EEPROM:0xB800 is cleared. This wipes out all special maps, routes, items, and pokemon. The watt counter and caught-pokemon area in the EEPROM are also cleared. Then the arrival animation and sound are played.
0x048C:startWalkEepromActions is interesting in of itself. It will write the reliable data at EEPROM:0x016F (the CopyMarker) with 0xA5. This allows the procedure to know to restart in case the battery is removed in the middle. Pretty clever actually. Then some extensive EEPROM copying is done. 0x2900 bytes are copied from the staging area at EEPROM:0xD700 to EEPROM:0x8F00. This data contains the route information as well as the graphics pertinent to the route and the items and pokemon available on it. Next, 0x280 bytes are copied from EEPROM:0xD480 to EEPROM:0xCC00. This is the team data. Team data is, actually, only 0x224 bytes, but the code is optimized of rcopying 0x80 bytes at a time, and thus the rounding up of size. The reliable data at EEPROM:0x016F (the CopyMarker) is then written with 0x00 to indicate that this procedure need not be resumed at next boot as it completed. The event log is then cleared by setting the eventType field in each entry to LogEvtEmptyEntry. Peer team information is cleared next, by erasing 0x1568 bytes at EEPROM:0xDE24. Some more flags are then shuffled in preparation for the walk. Finally, a log entry is added (of type LogEvtWalkStarted) to record the walk starting.
It is now obvious what the reliable data at EEPROM:0x016F (the CopyMarker) is for. If the battery is removed anytime during the long EEPROM copy of the staged data to the actual data locations, the marker will stay at 0xA5, and at next boot, this is checked for and the copy can be completed.
The communications are established, and then a CMD_2A is sent. That is all
After the communications are established, the DS will issue a CMD_20 to get the "IDENTITY DATA". It will see the walker is not paired and proceed to pair with it. CMD_32 will be issued to set the RTC. Then the images will be uploaded. This is everything in the range EEPROM:0x0280-EEPROM:0x8EFF. The images containing text will be the proper ones for the game's language. Thus the walker always adopts the language of the game it is paired with. Then, for some reason, the DS will send a ping (CMD_24). The walker will dutifully reply with a pong (CMD_26). The DS will then send CMD_38, which starts the walk in the walker, but in a different way than CMD_5A ("special event" status is not erased). Walker will reply with CMD_38 as well, and the DS will send a CMD_F4 to disconnect.
Here I'll describe cool things you can do that the DS game never does, but the walker supports
First, write 6 zeroes, followed by a little-endian 16-bit item number to EEPROM:0xBD40. Next, render the item name as a 2bpp 96x16 image, and upload it (0x180 bytes) to EEPROM:0xBD48. Then simply send CMD_C4. The game will show a cool animation of an item dropping in from nowhere. A special light-grey item icon will be shown in the inventory screen. The item does not occupy the space of the 3 dowsed items or the 10 gifted items. The event log will show an item appearing out of nowhere. The DS will also say that the pokemon is happy to have found such a rare item.
First, write a properly-filled out struct PokemonSummary to EEPROM:0xBA44. The pokemon may be shiny. This will work. Then, a properly-filled-out struct EventPokeExtraData to EEPROM:BA54. Here you have a chance to customize the "original trainer" name and IDs, the "Location met", pokemon's ability, and the pokeball type it is to be contained in. Next, you'll need to provide a 32x24 2-frame animation for the pokemon for the walker to show. Upload it (0x180 bytes) to EEPROM:0xBA80. And finally, render the pokemon's name as a 2bpp 80x16 image, and upload it (0x140 bytes) to EEPROM:0xBC00. Then simply send CMD_C2. The game will show a cool animation of a pokemon dropping in from nowhere. A special light-grey pokeball icon will be shown in the inventory screen. The pokemon does not occupy the space of the 3 radar-found pokemon. The event log will show a pokemon appearing out of nowhere. The DS will also say that the pokemon is happy to have found such a rare pokemon. You can see a video of this here.
From disassembling the DS game, I know the data pertinent to this is at EEPROM:0xB804 and is 0x240 bytes long, but I found no actual use of this data. The walker also does nothing with it other than showing a tiny "map" icon in the inventory screen. Nonetheless, the way to do this is to upload the data to EEPROM:0xB804 (or don't bother, the data is ignored). Then send CMD_C0. No event log is made and the DS will say nothing about this either. Sorry, I know no more.
Now, this is fun! You can craft a special route overlay over the current route the walker is on. What do I mean. You get to supply a new name, new image, and a special event pokemon and special event item that may be found on your route. You can set the step requirements and percent likelyhood of finding either, and you can also assign an event number to them, so that each can only be encountered once. This overlays the existing route available pokemon and items, thus after the procedure, the route now has 4 pokemon that can be encountered (instead of the traditional 3), and 11 items that might be dowsed (instead of the traditional 10). The special pokemon/items are tested for first, so as soon as the step count requirements are met, the percent chance is avaluated. If it is a hit, the item will be dowseable, and the pokemon may be radared. The special even pokemon format allows more data to be provided than is generally provideable for wild pokemon on route. Specifically, it is the same data as you can see described above in the "Directly gifting an event pokemon" section. If the player finds the event item, it goes into the special event item slot and does not occupy the one of the usual 3 items-found slots. A special event pokemon caught on the route also goes into a special slot, and does not occupy the space of one of the normal 3 caught-on-this-walk pokemon.
The setup is as follows: Upload a properly filled-out struct SpecialRoute to EEPROM:0xBF00. It contains much the same things as described in the above paragraphs, and a few extras. An image of the area to be shown on the home screen (32x24, used instead of EEPROM:0x8FBE) is required, and so is a rendered texual name of the area (80x16, 0x140 bytes). The command to send is CMD_C6. The walker will do the rest! The walker will stay on the special route until the walk is terminated by the DS.
You could also just replace the normal walk info at EEPROM:0x8F00, but this is cooler in some ways
The most obvious way to add watts is to simulate peer play repeatedly, until all ten gift item slots are filled, and then enjoy random small amount of watts, but there is a better way. Since we have arbitrary code execution, we can just call the func that adds watts: 0x1F3E:addWatts. It takes an 8-bit parameter in r0l register. If you want to add more watts at once, you can note that the first instruction of that function just extends the 8-bit value into the whole 16-bit r0 register. So jumping to 0x1F40 with a 16-bit watt amount in r0 will work.
I am not sure if anyone ever found the DS-side code for dealing with the PokéWalker. In any case, if not, here is the info for you. 0x20DDFE0 = irDoTx(u8* data, u32 len).0x20DDE94 = uint32_t irRx(u8* data) //returns len. The overlay that implements PokéWalker interaction is overlay9_112, which gets mapped at VA 0x21E5900. Some useful funcs: 0x21E5900 uint8_t packetCalcCrc(u8* data, u32 numBytes), 0x21E59B4 sendIrPacketEx(u8* data, u32 dataLen, u8 packetType, u8 packetExtraByte, u32 sessionID), 0x21E5A68 sendIrPacket(u8* data, u32 dataLen, u8 packetType, u8 packetExtraByte), 0x21E5B98 rxAndProcessIrPacket. It is actually a quite convoluted state, machine with too much indirection. The VA 0x21F4138 contains route info, formatted as follows.
Right away I knew that I did not want to offer anyone the binary download of the ROM (Nintendo being litigious bastards and all). But I also wanted to make it as easy as possible for others more brave than me to do so and to obtain it. Sadly there are not many hobbyist-accessible boards out there with IrDA transceivers. But you know what does have IrDA transceivers, is programmable, cheap, and available? PalmOS 4 devices! Palm m500 series to be exact (though others may work). They can be had for $5 on ebay, have SD cards, screens, and IR. So I wrote an app that dumps a PokéWalker ROM for you. Just point and click. It saves it on an SD card, called POKEROM.BIN.
I then extended the app to do a few other cool things, like gifting you any pokemon or item of your choice (including shiny, any moveset, etc) or adding 9999 Watts to your walker. It also serves as a sample implementation of the comms protocol and how to render text to images for the walker to use them properly. Nice and easy. The app, of course, is called "WalkePokér" and its sources and the binary are also included. It should work on any PalmOS 4 device, but only tested on the m5xx series. You can see some videos of it in action here and here. The license is GPL v3.
If you have never used a PalmOS device, here is the simplest way to use this one. Get an SD card of 1GB or smaller. Make a direcory called "PALM". In it, make one called "Launcher". Place the ".prc" file in there. Now when you insert the card, you'll see the icon for it in the Palm's Launcher.
I waffled a while about including my disassembly of the ROM, but given that most Pokémon games have their disassembly up on Github with no issues, I think it is OK. Keep in mind that many things here are still guesses and might be wrong.
Also my DECODEIMG utility's source is included. Its license is also GPLv3
DOWNLOAD