commaai/openpilot · error · RuntimeError

could not clear SPI block protection

Error message

could not clear SPI block protection

What it means

RuntimeError raised at the end of Flash.init(): the routine tries 5 times to clear the SPI status-register block-protection bits by issuing write-enable + write-status-register (command 0x01) with a zeroed 4-byte buffer, then reads back status(). If status() & 0x1C (block-protect bits) is still nonzero after 5 attempts, protection could not be cleared and any subsequent erase/program would silently fail.

Source

Thrown at openpilot/system/hardware/chestnut/flash.py:227

    while time.monotonic() < deadline:
      if not self.status() & 1:
        return
      time.sleep(0.005)
    raise TimeoutError("SPI flash WIP timeout")

  def init(self):
    self.reg_write(0xCC33, 0x04)
    self.reg_write(0xCA81, self.reg_read(0xCA81)[0] | 1)
    self.reg_write(0xC805, 0x02)
    self.reg_write(0xC8A6, 0x04)
    for _ in range(5):
      self.write_enable()
      self.write_buffer(bytes(4))
      self.transaction(0x01, length=1, addr_len=0x04, mode=1)
      time.sleep(0.01)
      if not self.status() & 0x1C:
        return
    raise RuntimeError("could not clear SPI block protection")

  def read(self, addr, length):
    out = bytearray()
    while len(out) < length:
      n = min(4096, length - len(out))
      self.transaction(0x03, addr + len(out), max(4096, n))
      for off in range(0, n, 255):
        out += self.reg_read(0x7000 + off, min(255, n - off))
    return bytes(out)

  def erase_sector(self, addr):
    self.write_enable()
    self.transaction(0x20, addr)
    self.wait_write_done()

  def program(self, addr, data):
    self.write_buffer(data + bytes((-len(data)) % 4))
    self.write_enable()

View on GitHub (pinned to 516ec1e682)

Solutions

  1. Power-cycle the enclosure (unplug/replug USB-C, or toggle VBUS) and retry the full connect()+init() sequence - a cold reset usually clears latched protection state
  2. Make sure init() runs on a fresh reconnect (close + claim again) so the controller registers are actually applied
  3. If it fails on every attempt with a stable link, read status() manually and compare the bit pattern against the flash datasheet - bits that never clear indicate a wrong flash part or dead chip
  4. Replace the enclosure if protection genuinely cannot be cleared (hardware fault)
Defensive patterns

Strategy: retry

Validate before calling

def protection_cleared(flash) -> bool:
    try:
        return not flash.status() & 0x1C
    except Exception:
        return False

Try / catch

try:
    flash.init()
except RuntimeError as e:
    if 'block protection' in str(e):
        power_cycle_enclosure()
        flash.connect(); flash.init()  # one cold-retry

Prevention

When it happens

Trigger: Flash.init() on a device whose status register stays protected: a write-enable that never latched (WEL dropped), a controller in the wrong mode because regs 0xCC33/0xCA81/0xC805/0xC8A6 were not applied to a live claim, or a hardware-protected or wrong-model flash chip.

Common situations: Running init() right after connect() on a half-enumerated device; status reads corrupted by a flaky USB link so the retry loop can never succeed; a flash chip variant with different protection semantics; device needing a power cycle to reset the status register.

Related errors


AI-assisted analysis of commaai/openpilot@516ec1e682 (2026-08-15). Data as JSON: /api/errors/7e576cee3afa78b3. Report an issue: GitHub.