PokéWalker hacking

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.

Table of Contents

  1. PokéWalker basics
    1. A short history
    2. User-facing features
    3. Cleverness
  2. The situation
    1. Problems
    2. The parts
    3. The CPU/SoC
  3. The comms protocol
    1. Basics
    2. The higher-level protocol
    3. Compression
  4. How to get in?
    1. Let's leak some data?
    2. Maybe we can overflow something?
    3. Decompression overflow?
    4. Perhaps decompression itself had bugs we can exploit?
    5. We have an opening!
    6. Towards an exploit!
  5. We have code execution!
    1. What's in a [shell]code?
    2. How do we dump?
  6. Let's analyze the ROM
    1. LCD identification & other misc hardware
    2. All the Commands
    3. Remote arbitrary code execution and CMD_06
    4. Over-cleverness
    5. Factory tests
  7. Pokémon game-specific info
    1. Structures
    2. EEPROM map
    3. Events
    4. Reliable data
    5. Normal processes
      1. How peer play works
      2. How walk start works
      3. How walker erasing works
      4. How walker pairing works
    6. Abnormal processes
      1. Directly gifting an event item
      2. Directly gifting an event pokemon
      3. Special map
      4. Special route
      5. Adding watts
    7. DS-side things
    1. The PalmOS app
    2. Disassembly
    3. Misc

PokéWalker basics

A short history

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.

User-facing features

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.

The situation


  • 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.

Image of the Pokewalker PCB

The parts

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 comms protocol


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.

The higher-level protocol

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.

How to get in?

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.

Let's leak some data?

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.

Maybe we can overflow something?

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.

Decompression overflow?

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.

Perhaps decompression itself had bugs we can exploit?

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...

We have an opening!

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?

Towards an exploit!

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:

0x10, //marker (unused) 0x06, //decompresed size (picked for proper decompressed length by trial, error, and some patience) 0, 0, //unused bytes 0x04, 0x3f, 0x00, 0x00, 0xf0, 0x00, //decompress the second stage decompressible data 0x70, 0x01, 0xff, 0xe0, //start of the 1st 17-byte spacer to produce a lot of zeroes 0x7f, 0x00, 0xb0, 0x01, //first spacer ends 0xe0, 0x10, //copy it for #2 (copy 17 from 17 back) 0xe0, 0x10, //copy it for #3 (copy 17 from 17 back) 0xe0, 0x10, //copy it for #4 (copy 17 from 17 back) 0xe0, 0x10, //copy it for #5 (copy 17 from 17 back) 0xe0, 0x10, //copy it for #6 (copy 17 from 17 back) 0xe0, 0x10, //copy it for #7 (copy 17 from 17 back) 0x80, 0xe0, 0x10, //copy it for #8 (copy 17 from 17 back) //shellcode (not compressed, zeroes interspersed since it is decompressed twice) 0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, 0, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0, 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0, 0xaa, 0x00, 0xaa, 0xfa, 0x80, 0xfa, 0x80, 0xfa, 0x80, 0, //last zero neeed - it guarantees that at end we're emitting byte-wise and thus can find a nice end-at address //now we need to consume some bytes and end on a good boundary in the original compressed buffer 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, //now we need to produce some spacing before the serious expansion starts. //Adjusting the repeat counts below can move the decompressed shellcode //in memory. 0xfe, 0x10, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0xc0, 0x00, 0xf0, 0x00, 0xf0, 0x00,

We have code execution!

What's in a [shell]code?

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:

1: mov.b @SCI.SSR3, r0l 28 9C //move also sets flags bpl 1b 4A FC //top bit is "TDRE" - TX buffer has space mov.b #0xCC, r0l F8 CC mov.b r0l, @SCI.TDR3 38 9B //write triggers transmission bra 1b 40 F6

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!

How do we dump?

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:

//set XX YY to start address mov.w #0xXXYY, r1 79 01 XX YY //long form of instruction to allow any 16-bit address 1: mov.b @SCI.SSR3, r0l 28 9C //move also sets flags bpl 1b 4A FC //top bit is "TDRE" - TX buffer has space mov.b @er1+, r0l 6C 18 //top bits of er1 are ignored, only bottom 16 matter. That's r1 mov.b r0l, @SCI.TDR3 38 9B //smuggle the byte out... bra 1b 40 F6

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!

dmitrygr@wvm:/root$ sha256sum POKE_recombobulated.bin f9e210a3b74afbbd12c5a66a51cc05cb9fbac986805ff0a3bfb4be6074d15607 POKE_recombobulated.bin

Let's analyze the ROM

Image of the console app

LCD identification & other misc hardware

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.

All the Commands

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.

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

Remote arbitrary code execution and CMD_06

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.

static const uint8_t rom_dump_exploit_upload_to_0xF956[] = { 0x56, //upload address low byte 0x5E, 0x00, 0xBA, 0x42, //jsr common_prologue 0x19, 0x55, //sub.w r5, r5 //lbl_big_loop: 0x79, 0x06, 0xf8, 0xd6, //mov.w 0xf8d6, r6 0xfc, 0x80, //mov.b 0x80, r4l 0x7b, 0x5c, 0x59, 0x8f, //eemov.b 0x79, 0x00, 0xaa, 0x80, //mov.w #0xaa80, r0 0x5e, 0x00, 0x07, 0x72, //jsr sendPacket 0x5E, 0x00, 0x25, 0x9E, //jsr wdt_pet 0x79, 0x25, 0xc0, 0x00, //cmp.w r5, #0xc000 0x46, 0xe4, //bne $-0x1c //lbl_big_loop 0x79, 0x00, 0x08, 0xd6, //mov.w #&irAppMainLoop, r0 0x5e, 0x00, 0x69, 0x3a, //jsr setProcToCallByMainInLoop 0x5a, 0x00, 0xba, 0x62 //jmp common_epilogue }; static const uint8_t trigger_uploaded_code_upload_to_0xF7E0[] = { 0xe0, //upload address low byte 0xf9, 0x56 //pointer to our code to run };


Idiotic code

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...

Factory tests

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.

Pokémon game-specific info


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.

struct UniqueIdentityData { //aka "UNIQUE ID DATA" u8 data[0x28]; //generated by the DS game at pairing time. unique per walker }; struct EventBitmap { //aka "EVENT BITMAP" u8 bitmap[0x10]; }; struct IdentityData { //aka "IDENTITY DATA" //stored reliably at 0x00ED/0x01ED //all multi-byte values LE, unless otherwise specified u32 unk_0; //written from game packet at walk start (LE, always 1?). u32 unk_1; //written from game packet at walk start (LE, always 1?). zeroed at walk end. when poke joins walk, copied from unk_0 u16 unk_2; //written from game packet at walk start (LE, always 7?). u16 unk_3; //written from game packet at walk start (LE, always 7?). zeroed at walk end. when poke joins walk, copied from unk_0 u16 trainerTID; u16 trainerSID; struct UniqueIdentityData uniq; struct EventBitmap evtBmp; u16 trainerName[8]; u8 unk_4 u8 unk_5; u8 unk_6; u8 flags; //0x01 - walker paired to game, 0x02 - walker has a pokemon, 0x04 - pokemon joined us on a walk u8 protoVer; //written by DS. we'll refuse to peer play with anyone with a mismatch. It is 0x02 u8 unk_7; u8 protoSubver; //written by DS. we'll refuse to peer play with anyone with a mismatch. It is 0x00 u8 unk_8; //written by DS at walk start tmie to 0x02 u32 lastSyncTime; //Big Endian u32 stepCount; //Big Endian }; struct PeerPlayData { //aka "PEER PLAY DATA" //all multi-byte values LE, unless otherwise specified u32 curStepCount; u16 curWatts; u8 padding_1[2]; u32 unk_0; //copied from IdentityDataCommon.unk_0 u16 unk_2; //copied from IdentityDataCommon.unk_2 u16 species; //species of the pokemon in this walker u16 pokeNickname[11]; u16 trainerName[8]; u8 pokeGenderForm; u8 pokeIsSpecial; //set if poke has forms: spinda, arceus, unown, etc }; struct LcdConfigCmds { //stored reliably at 0x00AC/0x01AC u8 contrastAndFlags; //if zero, commands in ROM at 0xBEB8 will be used instead u8 commands[0x3f]; //0xFE terminates command stream. 0xFD = delay. next byte is length }; struct EnrollData { //aka "ENROLL DATA" struct UniqueIdentityData uniq; struct LcdConfigCmds lcdCmds; u8 magix[8]; //written to EEPROM@0x0008 }; struct HealthData { //stored reliably at 0x0156/0x0256. Cached in RAM at 0xF780 //Big Endian unless otherwise noted u32 lifetimeTotalSteps; u32 todaySteps; //zeroed at midnight u32 lastSyncTime; u16 totalDays; u16 curWatts; u16 unk_0; u8 unk_1; u8 unk_2; u8 padding[3]; u8 settings; //bits[0] - isOnSpecial route, [1..2] - volume, [3..6] - contrast }; struct CopyMarker { //stored reliably at 0x016F/0x026F u8 marker; //0xA5 if a copy was interrupted. 0x00 else }; struct PokemonSummary { //LE u16 species; u16 heldItem; u16 moves[4]; u8 level; u8 variantAndFlags; //low 5 bits are variant (for unown, spinda, arceus, etc). mask 0x20 = female u8 moreFlags; //0x02 = shiny, 0x01 = has form u8 padding; }; struct EventPokeExtraData { //LE u32 unk_0; u16 otTid; u16 otSid; u16 unk_1; u16 locationMet; u16 unk_2; u16 otName[8]; u8 encounterType; u8 ability; u16 pokeballType; //as item number u8 unk[10]; }; struct TeamPokeData { //LE u16 species; u16 itemHeld; u16 moves[4]; u16 otTid; u16 otSid; u32 pid; u32 IVs; //packed to a u32. 5 bits each, LSB to MSB: hp, atk, def, speed, spAtk, spDef u8 EVs[6]; //hp, atk, def, speed, spAtk, spDef u8 variant; //for spinda, unown, arceus, etc u8 sourceGame; //seems related to game language where it was caught u8 ability; u8 happiness; u8 level; u8 padding; u16 nickname[10]; }; struct TeamData { //LE u8 unk_0[8]; struct UniqueIdentityData uniq; u16 tid; u16 sid; u8 unk_1[4]; //last byte likely player gender u16 name[8]; struct Unk { u32 flags; u16 val; u16 always_ffff; } unk_2[3]; struct TeamPokeData pokes[6]; u8 unknownZero[0x72]; u8 unknownData[10]; u8 unknownZero[0x1C]; u16 curPokewalkerRouteName[16]; u8 unknown [0x18]; }; enum RouteImageIdx { //while there are 27 routes, only 8 images are used. This is their index RouteImageFieldAndTrees, RouteImageForestAndTrees, RouteImageSuburbs, RouteImageUrban, RouteImageHillAndVolcano, RouteImageDimCave, RouteImageLake, RouteImageBeachAndWaves, }; enum LoggingEventType { LogEvtEmptyEntry, LogEvtPeerPlay1, LogEvtPeerPlay2, LogEvtPeerPlay3, LogEvtPeerPlay4, LogEvtPeerPlay5, LogEvtPeerPlay6, LogEvtPeerPlay7, LogEvtPeerPlay8, LogEvtPeerPlay9, LogEvtPeerPlay10, LogEvtItemDowsed, LogEvtItemDowsedSpecial, //special event item LogEvtPokeRadarCaught, LogEvtPokeRadarCaughtSpecial, //special event poke LogEvtPokeRadarRanAway, //0x0f LogEvtPokeRadarLost, //lost to radared poke 0x10 LogEvtPokeFoundItem, //idle poke finds item 0x11 LogEvtMoodHappySkippingAlong, LogEvtMoodRunningAroundInCirclesInDaze, LogEvtMoodLookingAway, LogEvtMoodBoredOrAngry, LogEvtMoodWantsToGoHome, LogEvtPokeJoinedYou, LogEvtWalkEnded, LogEvtWalkStarted, LogEvtPlayedLotFeelingContent LogEvtFellAsleep LogEvtItemGifted, }; struct EventLogItem { //MIXED ENDIANNESS, see each item for LE/BE designation u32 eventTime; //BE, seconds since jan 1 2001 u32 unk_0; //if peer-related event, PeerPlayData.unk_0, else zero u16 unk_2; //if peer-related event, PeerPlayData.unk_2, else zero u16 walkingPokeSpecies; //LE u16 caughtSpecies; //if this event is catching a poke, this is the species. if this event is for peer play - remote's species. LE u16 extraData; //if this is an item being found, item index. LE u16 remoteTrnrName[8]; //if this event is for peer play - remote's trainer name. LE u16 pokeNick[11]; //nickname of walking poke. LE u16 remPokeNick[11]; //if this event is for peer play - remote's nickname of walking poke. poke radar pokes can also have a nick here... LE u8 routeImageIdx; //enum RouteImageIdx u8 pokeFriendship; //at time of this event u16 watts; //watts at this time. BE u16 remoteWatts; //if this event is for peer play - remote's watts. BE u32 stepCount; //BE u32 remoteStepCount; //if this event is for peer play - remote's step count. BE u8 eventType; //enum LoggingEventType u8 genderAndForm; //bottom 5 bits: form (unown, spinda, arceus), next 2: gender: {M, F, none}, top bit: 1 if special poke that has form u8 caughtGenderAndForm; //if this event is catching a poke, same format as .genderAndForm. if peer play, peer's gender and form u8 padding; }; struct RouteInfo { //LE //waking poke info struct PokemonSummary poke; u16 nickname[11]; u8 friendship; //route info u8 routeImageIdx; //enum RouteImageIdx u16 routeName[21]; //route-encounterable pokes struct PokemonSummary routePokes[3]; u16 routePokeMinSteps[3]; //minimum steps to encounter each pokemon in the above structs. checked in order u8 routePokeChance[3]; //percent likelyhood to meet above poke once step minimums met. checked in order u8 pad1; //route-findable items u16 routeItems[10]; //items we may find on this route u16 routeItemMinSteps[10];//minimum steps to find each item in the above array. checked in order u8 routeItemChance[10]; //percent likelyhood to fine above item once step minimums met. checked in order }; struct SpecialRoute { //LE u8 itemInfoUnused[6]; u8 routeImageIdx; //enum RouteImageIdx u8 paddingl; struct PokemonSummary specialPoke; struct EventPokeExtraData specialPokeExtra; u16 minStepsForSpecialPoke; u8 percentChanceSpecialPoke; u8 padding2; u16 specialItem; u16 minStepsForSpecialItem; u8 percentChanceSpecialItem; u8 padding3[3]; u16 specialRouteName[21]; u8 pokeEvtNum; //zero if none. else only catchable once per savefile u8 itemEvtNum; //zero if none. else only obtainable once per savefile u8 pokeAnimatedSmallImg[0x170]; //should be 0x180, truncated (for space?) u8 pokeNameImage[0x140]; u8 areSmallImage[0xc0]; u8 areaTextNameImg[0x140]; u8 itemNameImg[0x180]; } struct RandomCheckInfo { u16 adrOfst; //LITTLE ENDIAN, from 8CF0!!! u8 numBytes; u8 sum; };


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 - 0x000Fsome value written during personalization. Never read.
0x0010 - 0x0071???
0x0072Number of watchdog resets
0x0073 - 0x007f???
0x0080-0x0082factory-provided ADC calibration data. (RELIABLE DATA FORMAT, copy at 0x0180)
0x0083-0x00ABstruct UniqueIdentityData. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x0183)
0x00AC-0x00ECstruct LcdConfigCmds. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x01AC)
0x00ED-0x0155struct IdentityData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x01ED)
0x0156-0x016Estruct HealthData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x0256)
0x016F-0x0171struct CopyMarker. Used at walk init time (RELIABLE DATA FORMAT, copy at 0x26F)
0x0180-0x0182factory-provided ADC calibration data. (RELIABLE DATA FORMAT, copy at 0x0080)
0x0183-0x01ABstruct UniqueIdentityData. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x0083)
0x01AC-0x01ECstruct LcdConfigCmds. Provisioned at game pairing time (RELIABLE DATA FORMAT, copy at 0x00AC)
0x01ED-0x0255struct IdentityData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x00ED)
0x0256-0x026Estruct HealthData. Provisioned at walk start time (RELIABLE DATA FORMAT, copy at 0x0156)
0x026F-0x0271struct CopyMarker. Used at walk init time (RELIABLE DATA FORMAT, copy at 0x16F)
0x0280-0x041FNumeric character images: "0123456789:-/", 8x16 each, in this order
0x0420-0x045FWATT symbol image 16x16
0x0460-0x046Fpokeball 8x8
0x0470-0x047Fpokeball light grey 8x8 (used for event pokemon)
0x0488-0x0497item symbol 8x8
0x0498-0x04A7item symbol light grey 8x8 (used for event items)
0x04A8-0x04B7tiny map icon 8x8 (used for "special map" reception)
0x04B8-0x04F7card faces: heart, spade, diamond, club, 8x8 each (used for "stamp" reception)
0x04F8-0x05B7arrows (up down left right), each in 3 configs (normal, offset, inverted) 8x8 each
0x05B8-0x05D7left arrow for menu 8x16
0x05D8-0x05F7right arrow for menu 8x16
0x05F8-0x0617"return" symbol for menu 8x16
0x0638-0x0647symbol for "have more message" in the bottom right of messages. ORRED into last 8 columns (thus 16 bytes). Applied after 0x0648
0x0648-0x064Fsymbol 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-0x065Fmedicine vial (?) icon 8x8
0x0660-0x066Flow battery icon 8x8
0x0670-0x08AFlarge talk bubbles from bottom right with pokemon feeling icon (exclamation, heart, music note, smile, neutral face, ellipsis) 24x16 each, 6 of them
0x08B0-0x090Flarge 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-0x138Ftrainer's name rendered as an image 80x16
0x1390-0x13CFsmall 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-0x182Fspeaker icon with no waves (no sound) for preferences screen 24x16
0x1830-0x188Fspeaker icon with one wave (low sound) for preferences screen 24x16
0x1890-0x18EFspeaker icon with two waves (high sound) for preferences screen 24x16
0x18F0-0x190Fcontrast demonstrator (drawn a bunch of times over) 8x16
0x1910-0x19CFlarge treasure chest icon for item view 32x24
0x19D0-0x1A8Flarge map scroll thingie 32x24
0x1A90-0x1B4Flarge present icon for item view 32x24
0x1B50-0x1B8Fsmall bush dark-colored, for dowsing 16x16
0x1B90-0x1BCFsmall bush light-colored, for dowsing 16x16
0x1BD0-0x1C4F"LEFT: "string on white background. seems unreferenced 32x16
0x1C50-0x1CAFblank image 16x24
0x1CB0-0x1C6Fbush dark 32x24
0x1D70-0x1DAFword bubble with one exclamation point (for poke hunting) 16x16
0x1DB0-0x1DEFword bubble with two exclamation points (for poke hunting) 16x16
0x1DF0-0x1E20word bubble with three exclamation points (for poke hunting) 16x16
0x1E30-0x1E6Fthree lines radiating from bottom left (for bush we just clicked) 16x16
0x1E70-0x1EEFskewed small 7-pointed star (attack) 16x32
0x1EF0-0x1F6Fskewed large 7-pointed star (critical hit attack) 16x32
0x1F70-0x202Fcloud "for pokemon appeared" 32x24
0x2030-0x203F"HP" item (4 of these make up an HP bar) 8x8
0x2040-0x204Fa little 5-pointed star image for when we catch something 8x8
0x2050-0x234F"attack/evade/catch" directions placard for battles 96x32
0x2350-0x244Fpokewalker image, blank screen, 32x32
0x2450-0x246FIR xmit icon (like wifi arcs) 8x16
0x2470-0x247Fmusic note icon 8x8
0x2480-0x248Fblank 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
0x8CB0-0x8CDFrandom checksum area descriptor addrs (see 0x36F2:randomEepromChecksumCheck, struct struct RandomCheckInfo)
0x8CF0-0x8EFFrandom garbage data that is checksummed by randomEepromChecksumCheck()
0x8F00-0x8FBDstruct RouteInfo - current route data
0x8FBE-0x907Dcurrent "area" we are strolling in graphic 32x24
0x907E-0x91BDcurrent "area" we are strolling in textual name 80x16
0x91BE-0x933Dcurrent pokemon animated sprite for "held items and pokemon" screen, fights, etc. 32 x 24 x 2 frames
0x933E-0x993Dcurrent pokemon large nimated sprite for main screen 64 x 48 x 2 frames
0x993E-0x9A7Dcur pokemon name image 80x16
0x9A7E-0x9EFDroute available pokemon selected by the same animated small sprites 32 x 24 x 2 frames x 3 pokemon
0x9EFE-0xA4FDlarge 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-0xA8BDavailable pokemon name images 80x16 x3 pokemon
0xA8BE-0xB7BDitem names as images. 96x16 x 10 images (one per item)
0xB800Bitfield 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"
0xB804-0xBA43data for "special map received". Format unknown. Possibly used by the DS games, but no evidence of this found in the games.
0xBA44-0xBA53gifted event poke, or radar-caught event pokebasic data. struct PokemonSummary
0xBA54-0xBA7Fextra data. struct EventPokeExtraData
0xBA80-0xBBFFsmall sprite 32 x 24 x 2 frames
0xBC00-0xBD3Fname image 80x16
0xBD40-0xBD47gifted event item, or dowsed event itemitem data. 6 bytes of zeroes, then u16 item, LE
0xBD48-0xBEC7item name image 96x16
0xBF00-0xBF05"special route" info (struct SpecialRoute):6 bytes of zeroes (part of item struct but unused by DS or walker)
0xBF06enum RouteImageIdx
0xBF08-0xBF17special route-available pokemon basic data. struct PokemonSummary
0xBF18-0xBF43special route-available pokemon extra data. struct EventPokeExtraData
0xBF44-0xBF45min steps to encounter this poke on the route. u16 LE
0xBF46percent chance to encounter this poke on route after step minimum met
0xBF48-0xBF49special route-available item. u16 LE
0xBF4A-0xBF4Bmin steps dowse this item. u16 LE
0xBF4Cpercent chance to dowse this item on route after step minimum met
0xBF50-0xBF79routeName u16[21]
0xBF7A"event index" for catching this route's special pokemon
0xBF7B"event index" for dowsing this route's special item
0xBF7C-0xC6FBspecial 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-0xC83Bspecial route pokemon name image 80x16
0xC83C-0xC8FBspecial routes's large image for home screen, like 0x8FBE is for a normal route 32x24
0xC8FC-0xCA3Bspecial routes's textual name 80x16
0xCA3C-0xCBBBspecial route item textual name 96x16
0xCC00-0xCE23struct 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-0xCE7Falso written at walk start time as part of the above. probably just to keep the write a multiple of 0x80 bytes
0xCE88If low bit set, game will give player a STARF berry ONCE per savefile. Used when 99999 steps reached
0xCE8A-0xCE8Bcurrent watts written to eeprom by cmd 0x20 before replying (likely so remote can read them directly). u16 BE
0xCE8C-0xCEBB3x route-available pokemon we've caught so far. 3x struct PokemonSummary
0xCEBC-0xCEC73x route-available items we've dowsed so far. 3x {u16 LE item, u16 LE unused}
0xCEC8-0xCEEF10x route-available items we've been gifted by peer play. 3x {u16 LE item, u16 LE unused}
0xCEF0-0xCF0Bhistoric step count per day. u32 each, BE, [0] is yesterday, [1] is day before, etc...
0xCF0C-0xD47FEvent log. Circularly-written, displayed in time order. 24x struct EventLogItem
0xD480-0xD6FFteam data written here before walk start action. struct TeamData
0xD700-0xDBCBscenario data written here before walk start action. everything that 0x8F00-0xB7FF would have
0xDC00-0xDE23current peer play peer. struct TeamData. uploaded as part of peer play. later shifted to index [0] at 0xDE24 list of peers
0xDE24-0xF38BPeers we've met. For battle house info. Newest element is first. 10x struct TeamData
0xF400-0xF57Fpeer play temporary data about peermedium pokemon animated image of pokemon we are peer-playing with (never erased) 32x24 x 2 frames
0xF580-0xF6BFrendered text name of pokemon we are peer-playing with 80x16
0xF6C0-0xF6F7data. 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.

Reliable data

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

Normal processes

How peer play works

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.

How walk start works

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.

How walker erasing works

The communications are established, and then a CMD_2A is sent. That is all

How walker pairing works

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.

Abnormal processes

Here I'll describe cool things you can do that the DS game never does, but the walker supports

Directly gifting an event item

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.

Directly gifting an event pokemon

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.

Special map

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.

Special route

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

Adding watts

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.

DS-side things

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.

static const char mImgNames[][13] = { "UNKNOWN", "Field&Trees", "Forest&Trees", "Suburbs", "Urban", "Hill&Volcano", "Dim Cave", "Lake", "Beach&Waves", }; static const char mTypeNames[][9] = { "Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost", "Steel", "( ? ? ? )", "Fire", "Water", "Grass", "Electric", "Psycic", "Ice", "Dragon", "Dark" }; struct RouteItemInfo { uint16_t item; //0x00 uint16_t minSteps; //0x02 uint8_t percentChance; //0x04 uint8_t padding; }; //0x06 struct RoutePokeInfo { uint16_t poke; //0x00 uint8_t level; //0x02 uint8_t padding1; uint16_t heldItem; //0x04 uint8_t unsure; //0x06 - usually zero uint8_t flags; //0x07 - low bit = female uint16_t moves[4]; //0x08 uint16_t minSteps; //0x10 uint8_t encounterChancePercent; //0x12 uint8_t padding2; }; //0x14 struct RouteInfo { uint32_t wattsToUnlock; //0x00 uint32_t imageIdx; //0x04 struct RoutePokeInfo pokes[6]; //0x08 struct RouteItemInfo items[10]; //0x80 uint8_t advantagedTypes[3]; //0xBC //mTypeNames uint8_t padding; }; //0xC0


WalkePoker PalmOs app screenshot

The PalmOS app

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.


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



© 2012-2020