Recovering from Invalid ROM table on an SNR8503M

27 May 2026 - tsp
Last update 27 May 2026
Reading time 12 mins

Ever got an Invalid ROM table for your Snaner SNR8503M after playing around with your own flashing scripts, trying to migrate from the annoying closed Keil toolchain to open gcc, reverse engineering the FLM file, and generally making questionable life choices due to lack of time and too much curiosity?

Then this article might save you a fewĀ cents on your microcontroller.

In my case the chip was neverĀ really dead as most people I talked to suggested - actually the typical suggestion was to throw the chip away and replace it, a very bad trend. Instead, the Cortex-M0 CoreSight / MEM-AP infrastructure simply got into a state where memory reads returned stale or aliased values. The annoying Keil environment then failed with a non-telling message:

Invalid ROM table

OpenOCD behaved similarly and Cortex auto detection failed.Ā At first sight this looks like a permanently corrupted chip or damaged ROM. It is not. Spoiler: Actually the view of the ROM is not read only and you have just overmapped the region during the flashing attempts. This yields the toolchains to simply refuse to interact with your microcontroller.

The device can actually be recovered.

The Hardware

The board that I had the experience with itself is surprisingly good:

The documentation on the other hand was … minimalistic.Ā The chip used here was an SNR8503M, which internally appears to be related to the LKS32MC03x family. Those chips do not appear in Europe very often, but they are pretty common in low cost motor controllers from China.

Important upfront:

What Was Needed

In case you think about trying with STLink devices just forget it. They seem to filter for STM32 and STM8 device families even though there is no real reason not to support other devices. It’s just an active filter, not missing features. In this case I would prefer some cheap CMSIS-DAP clone, those actually work and offer more flexibility.

How the Device Initially Got Bricked

The original goal was replacing the Keil flashing workflow with a custom Python implementation and the firmware for the controller, that was luckily open source, with an implementation that works directly with the gcc compiler for ARM. I also extended the firmware with custom startup for a turbomolecular pump.Ā That task involved:

Initially this worked surprisingly well.Ā Until it suddenly did not. I made the mistake of writing back an supposed complete dump, that was much larger than the flash memory area, writing into arbitrary memory regions in the memory mapped space. This worked on the first try - but only on a device that was freshly reset and never executed firmware. The second time the memory controllers where already configured and thus the side effects where different.

After the last flashing attempt:

Example:

0x00000000: 3b02c347 3b02c347 3b02c347 ...
0xe000ed00: 3b02c347 3b02c347 3b02c347 ...

or later:

0x20000000 -> 44444444
0x20000004 -> 44444444
0x20000008 -> 44444444

Independent addresses returned identical values.Ā The ROM table itself was not actually destroyed.Ā The debug bus had simply entered a completely broken state that was preserved over power cycling.

Recovering

Recovering was - in hindsight - very simple:

After those steps the chip partially was able to boot (until it double faulted (repeatedly entered HardFault) on corrupted memory regions).

OpenOCD Configuration

The following OpenOCD configuration turned out to be sufficient:

source [find interface/cmsis-dap.cfg]

cmsis-dap quirk enable
transport select swd
adapter speed 5

set _CHIPNAME snr8503

swd newdap $_CHIPNAME cpu -expected-id 0x0bb11477
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu

target create $_CHIPNAME.ap mem_ap -dap $_CHIPNAME.dap -ap-num 0

init

Important details:

Talking to OpenOCD from Python

Instead of directly implementing CMSIS-DAP packets, Python simply connected to OpenOCD over telnet:

import socket
import re

class OpenOCD:
    def __init__(self, host="127.0.0.1", port=4444, timeout=3.0):
        self.s = socket.create_connection((host, port), timeout=timeout)
        self.s.settimeout(timeout)
        self._read_until_prompt()

    def close(self):
        self.s.close()

    def _read_until_prompt(self):
        data = b""
        while True:
            chunk = self.s.recv(4096)
            if not chunk:
                break
            data += chunk
            if data.rstrip().endswith(b">"):
                break
        return data.decode(errors="replace")

    def cmd(self, line):
        self.s.sendall((line + "\n").encode())
        return self._read_until_prompt()

    def mdw1(self, addr):
        out = self.cmd(f"mdw 0x{addr:08x} 1")
        m = re.search(r"0x[0-9a-fA-F]+:\s+([0-9a-fA-F]{1,8})", out)
        if not m:
            raise RuntimeError(f"mdw failed:\n{out}")
        return int(m.group(1), 16)

    def mww(self, addr, value):
        out = self.cmd(f"mww 0x{addr:08x} 0x{value:08x}")
        if "Failed" in out or "Error" in out:
            raise RuntimeError(f"mww failed:\n{out}")

    def dap(self, command):
        return self.cmd("snr8503.dap " + command)

The Key Discovery

The critical insight was, thatĀ the .FLM itself already contained the proper very simple erase sequence. An FLM simply is code that is loaded by the uploader into the device memory upfront flashing. The FLM provides a set of standardized callable methods that actually implement the flashing routines. This way the IDE can transfer the uploaded sectors into flash memory without the toolchains knowing how memory frameworks are actually implemented on the device side. The code is usually lightweight ARM bytecode.Ā Disassembling the FLM revealed the magic constants:

0x7A83
0x8FCA
0x7654DCBA

and the relevant flash FSM registers:

0x400000A8
0x40000080
0x400000D0
0x00010000
0x00010010
0x00010014
0x00010018

The Actual Recovery

The following erase sequence magically restored the ROM table - by erasing the whole chip and thus also recovering the persistent configuration of the bus system:

def try_chip_erase_from_flm(ocd):
    ocd.mww(0x400000A8, 0x00007A83)

    v = ocd.mdw1(0x40000080)
    ocd.mww(0x40000080, v | 0x000001FF)

    ocd.mww(0x400000D0, 0x00008FCA)

    v = ocd.mdw1(0x00010000)
    ocd.mww(0x00010000, v | 0x80008000)

    ocd.mww(0x00010010, 0x7654DCBA)

    for _ in range(1000):
        done = ocd.mdw1(0x00010018)
        if done == 1:
            fail = ocd.mdw1(0x00010014)
            print(f"Erase done, fail={fail}")
            return fail == 0

After executing this sequence:

OpenOCD suddenly reported:

Part is 0x471, Cortex-M0 ROM (ROM Table)
Part is 0x00a, Cortex-M0 DWT
Part is 0x00b, Cortex-M0 BPU

instead of invalid garbage.

Why This Works

The ROM table was never physically corrupted.Ā Instead the the flash FSM or internal configuration entered an inconsistent state andĀ the mem-ap returned stale values.Ā Keil interpreted those values as a broken ROM table.Ā The chip erase reset the internal state machine and restored proper CoreSight operation. Tooling just refused to execute the sequence without connecting to the core (which is required to execute the FSM on the device).

A Minor Quirk After Restoring the ROM Table

Trying to flash the firmware again with Keil after the ROM table appeared again - just broke again. The uploader executed erase chip correctly, it even started to transfer data. Just to abort. Giving this a quick glance with OpenOCD again showed the reason was the microcontroller double faulting. It just started to execute invalid code and the uploader simply did not halt the CPU correctly. The quick fix was to just increase flashing speed. This pushed in enough data to get more headroom till the uploader issued the halt. After a complete flash - the chip vanished and looked dead.

Final Surprise

After successfully flashing the - in hindsight -Ā wrong firmware image, the debug interface vanished again.Ā This turned out not to be another brick as I immediately suspected.Ā The firmware simply repurposed the SWD/ICP pins for board temperature monitoring.

The debugger pins became thermometer pins.

This explained why the interface disappeared completely afterwards. Of course a fix was trivial after recognizing what happened by pulling down the ICPCS pin to GND during the reset, then attaching the debugger and executing the flash again - this time with firmware that did not touch the SWD.

Conclusion

The important takeaway:

ā€œInvalid ROM tableā€ does not necessarily mean permanent hardware damage.

If the SWD DPIDR still answers the device is recoverable.

In myĀ case it was very simple in hindsight:

So before throwing away any board or microcontroller:

References

This article is tagged:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplainsQu98equt9ewh@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support