Skip to content

Instantly share code, notes, and snippets.

@k9ert
Created February 3, 2026 15:59
Show Gist options
  • Select an option

  • Save k9ert/2b426dade405845b868d62e17aac5a06 to your computer and use it in GitHub Desktop.

Select an option

Save k9ert/2b426dade405845b868d62e17aac5a06 to your computer and use it in GitHub Desktop.
Specter-DIY STM32F469 + MicroPython + LVGL 9.x debugging notes

Debugging Python Boot Issues

Date: 2024-12-19

Problem

When MicroPython firmware fails during boot (e.g., due to import errors in frozen modules), the board may appear dead with no visible error output. Debug builds have REPL enabled, but if the main script crashes on import, you won't see the stacktrace unless you manually trigger the import.

Solution: Iterative Debug Workflow

Use disco repl import to manually import the boot module and see any stacktraces.

Step-by-Step Process

  1. Build debug firmware

    make debug
  2. Flash to board

    scripts/disco flash program bin/debug.bin --addr 0x08000000
  3. Test boot sequence

    scripts/disco repl import

    This will:

    • Stop OpenOCD if running
    • Wait for USB CDC device
    • Import hardwaretest (the default boot module)
    • Show any stacktraces
  4. Analyze error - Example output:

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "hardwaretest.py", line 1, in <module>
      File "gui/__init__.py", line 1, in <module>
      File "gui/components/__init__.py", line 3, in <module>
      File "gui/components/keyboard.py", line 7, in <module>
    AttributeError: 'module' object has no attribute 'btnm'
    
  5. Fix the issue in source code

  6. Repeat from step 1 until no errors

Testing Specific Modules

You can test individual modules to isolate issues:

# Test just the GUI module
scripts/disco repl import gui

# Test specific component
scripts/disco repl import gui.components.qrcode

# Test with longer timeout for slow imports
scripts/disco repl import hardwaretest -t 30

Import Chain

Understanding the import chain helps locate issues:

boot.py
  └── pyb.main("hardwaretest.py")
        └── hardwaretest.py
              └── gui/__init__.py
                    └── gui/core.py
                          └── gui/common.py
                                └── gui/components/__init__.py
                                      ├── gui/components/qrcode.py
                                      ├── gui/components/keyboard.py
                                      └── ...

Example: LVGL API Migration

When upgrading LVGL versions, many API changes cause boot failures:

Issue 1: Style API (qrcode.py)

Error:

AttributeError: 'lv_style_t' object has no attribute 'body'

Fix: LVGL 8.x replaced style.body.main_color with style.set_bg_color():

# Old (LVGL 7.x)
qr_style.body.main_color = lv.color_hex(0xFFFFFF)

# New (LVGL 8.x)
qr_style.init()
qr_style.set_bg_color(lv.color_hex(0xFFFFFF))

Issue 2: Button Matrix (keyboard.py)

Error:

AttributeError: 'module' object has no attribute 'btnm'

Fix: lv.btnm renamed to lv.buttonmatrix:

# Old
class HintKeyboard(lv.btnm):

# New
class HintKeyboard(lv.buttonmatrix):

Issue 3: QSPI Directory Shadowing Frozen Modules

Error:

ImportError: no module named 'hosts.QRHost'

Diagnosis: Check /qspi for directories with same names as frozen packages:

scripts/disco repl exec "import os; print(os.listdir('/qspi'))"
# If you see 'hosts', 'gui', etc. - they're shadowing frozen modules

Fix: Remove the shadowing directories:

scripts/disco repl exec "import os; os.rmdir('/qspi/hosts')"
# Reset board after removing
scripts/disco ocd connect && scripts/disco cpu reset && scripts/disco ocd disconnect

Root cause: Old app installations leave empty directories on QSPI that get found before frozen modules in sys.path.

Tips

  • Check LVGL API: Use scripts/disco repl exec "import lvgl as lv; print(dir(lv))" to see available classes
  • Check style methods: scripts/disco repl exec "import lvgl as lv; s = lv.style_t(); print(dir(s))"
  • Incremental testing: Fix one error at a time, rebuild, and test again
  • Keep REPL alive: Debug builds keep REPL active even after import failures, allowing further investigation
  • Check sys.path: scripts/disco repl exec "import sys; print(sys.path)" - order matters for imports

Device Recovery - 2024-12-19

Symptoms

  • Black screen
  • No USB CDC (REPL unavailable)
  • Only ST-LINK VCP visible
  • Even old known-working firmware failed to boot

Root Cause

QSPI flash contained shadow directories (/qspi/hosts, /qspi/wallets, etc.) that were shadowing frozen modules. After MicroPython upgrade, the import system tried to use these empty directories as packages, causing crash before USB init.

What Failed

1. Flashing old firmware to 0x08020000

scripts/disco flash program fwbox/old/debug/debug.bin --addr 0x08020000
  • Flash verified OK
  • Device still crashed
  • Shadow dirs on QSPI persisted

2. Flashing new firmware with Fix D

  • Same result - QSPI state was corrupted

3. OpenOCD timeouts

  • Frequent "OpenOCD connection failed: timed out"
  • Required multiple reconnect attempts

What Worked

1. Mass erase internal flash

echo "yes" | scripts/disco flash erase

WARNING: This erases bootloader too!

2. Flash FULL firmware from 0x08000000

After mass erase, PC showed 0xfffffffe (hard fault - no bootloader).

Must flash full image including bootloader:

scripts/disco flash program fwbox/old/main/specter-diy.bin --addr 0x08000000

Note: Use --addr 0x08000000 not 0x08020000

3. Device recovered

  • Display working
  • Note: QSPI may need reformatting (mass erase only affects internal flash)

Key Lessons

  1. Mass erase deletes bootloader - must reflash from 0x08000000
  2. QSPI shadow dirs can brick device - even with correct firmware
  3. Flash verify passes but device crashes - issue is in QSPI, not internal flash
  4. Keep full firmware images in fwbox/old/main/ for recovery

Recovery Checklist

If device is unresponsive:

  1. scripts/disco flash verify <firmware> --addr 0x08020000 - check flash integrity
  2. If verify OK but still crashes → QSPI corruption
  3. Mass erase: echo "yes" | scripts/disco flash erase
  4. Flash full image: scripts/disco flash program <full.bin> --addr 0x08000000
  5. Device should boot (QSPI will be reformatted on first boot)

Finder Flashing Bug

Date: 2024-12-19 Board: STM32F469 Discovery Firmware: fwbox/spflashbug/debug.bin (1,796,264 bytes)

Summary

Flashing firmware via Finder (drag-drop to DIS_F469NI mass storage) truncates the last ~1,700 bytes compared to flashing via OpenOCD/JTAG.

Test Methodology

  1. Flash spflashbug/debug.bin via scripts/disco flash program --addr 0x08000000
  2. Verify with disco flash verify --smart → PASSED
  3. Flash same file via Finder (drag to DIS_F469NI drive)
  4. Verify again → FAILED

Findings

Region Disco (OpenOCD) Finder (Mass Storage)
Bootloader @ 0x08000000 (16KB) Match Match
Firmware @ 0x08020000 (1.6MB) Match Truncated

Byte-level comparison

Firmware region size: 1,665,192 bytes
Bytes written by Finder: 1,663,488 bytes
Missing: 1,704 bytes at end

Offset 0x001961f8 (byte 1,663,480):
  Expected: 3e69 0e08 143a 1608 0000 0000 0300 0000
  Actual:   3e69 0e08 143a 1608 ffff ffff ffff ffff
                                ^^^^ ^^^^ ^^^^ ^^^^
                                Erased (0xFF), should be data

Root Cause

Unknown. Likely ST-LINK mass storage interface limitation or bug. The DIS_F469NI virtual drive doesn't reliably write the complete file to flash.

Reproducibility

Intermittent. Second test with same file via Finder passed verification. This makes the bug harder to detect - sometimes it works, sometimes it truncates.

  • Test 1 (Finder): FAILED - truncated 1,704 bytes
  • Test 2 (Finder): PASSED - all bytes correct

Recommendation

Do not use Finder/drag-drop for flashing. Use OpenOCD via scripts/disco flash program instead. Always verify after flashing.

OpenOCD Reliability Test

All fwbox binaries flashed and verified via OpenOCD - 100% success rate:

Firmware Size Result
upy-f469disco.bin 1,303,816 PASSED
specter-diy-tadeu-autobuild.bin 1,485,072 PASSED
new/debug/debug.bin 1,796,232 PASSED
new/main/specter-diy.bin 1,794,120 PASSED
old/main/specter-diy.bin 1,481,976 PASSED
spflashbug/debug.bin 1,796,264 PASSED

Verification Commands

# Flash via OpenOCD (reliable)
scripts/disco flash program firmware.bin --addr 0x08000000

# Verify (use --smart for files with zero-preserved regions)
scripts/disco flash verify firmware.bin --addr 0x08000000 --smart

LVGL 9.3.0 Migration Lessons Learned

Date: 2024-12-19

Overview

Migration from LVGL 7.x to LVGL 9.3.0 in MicroPython environment. Major API changes across theming, styling, events, and widgets.

Critical API Changes

1. Screen Functions

# Old (LVGL 7.x)
lv.scr_act()
lv.scr_load(scr)

# New (LVGL 9.x)
lv.screen_active()
lv.screen_load(scr)

2. Object Alignment

# Old (LVGL 7.x) - 5 arguments
obj.align(reference, lv.ALIGN.OUT_BOTTOM_MID, x_ofs, y_ofs)

# New (LVGL 9.x) - 4 arguments, use align_to for relative alignment
obj.align_to(reference, lv.ALIGN.OUT_BOTTOM_MID, x_ofs, y_ofs)
obj.align(lv.ALIGN.CENTER, x_ofs, y_ofs)  # align to parent

3. Alignment Constants

# Old → New
lv.ALIGN.IN_TOP_MIDlv.ALIGN.TOP_MID
lv.ALIGN.IN_TOP_RIGHTlv.ALIGN.TOP_RIGHT
lv.ALIGN.IN_TOP_LEFTlv.ALIGN.TOP_LEFT
lv.ALIGN.IN_BOTTOM_MIDlv.ALIGN.BOTTOM_MID
lv.ALIGN.IN_LEFT_MIDlv.ALIGN.LEFT_MID
lv.ALIGN.IN_RIGHT_MIDlv.ALIGN.RIGHT_MID
# OUT_* constants remain the same

4. Event Callbacks

# Old (LVGL 7.x) - callback receives (obj, event_code)
def callback(obj, event):
    if event == lv.EVENT.CLICKED:
        do_something()
btn.set_event_cb(callback)

# New (LVGL 9.x) - callback receives event object
def callback(event):
    code = event.get_code()
    target = event.get_target()
    if code == lv.EVENT.CLICKED:
        do_something()
btn.add_event_cb(callback, lv.EVENT.CLICKED, None)

Important: If registering for specific event (e.g., lv.EVENT.CLICKED), the callback only fires for that event. Check for matching event code in callback.

5. Theme Initialization

# Old (LVGL 7.x)
th = lv.theme_night_init(hue, font)
th = lv.theme_material_init(hue, font)
lv.theme_set_current(th)

# New (LVGL 9.x)
disp = lv.display_get_default()
th = lv.theme_default_init(disp, primary_color, secondary_color, dark_mode, font)
disp.set_theme(th)

6. Style System

# Old (LVGL 7.x) - hierarchical style access
th.style.btn.rel.body.main_color = color
lv.style_copy(new_style, old_style)
obj.set_style(style)
obj.set_style(lv.btn.STYLE.REL, style)

# New (LVGL 9.x) - flat style with set_* methods
style = lv.style_t()
style.init()  # IMPORTANT: must call init()
style.set_bg_color(color)
style.set_radius(10)
style.set_border_width(0)
obj.add_style(style, 0)  # 0 = default state selector
obj.add_style(style, lv.STATE.PRESSED)  # for pressed state

7. Widgets Removed/Changed

# lv.page removed - use lv.obj with scrolling
# Old
self.page = lv.page(parent)

# New - regular obj is scrollable by default
self.page = lv.obj(parent)

# lv.btn renamed
# Old
btn = lv.btn(parent)

# New
btn = lv.button(parent)

# lv.btnm renamed
# Old
class MyWidget(lv.btnm):

# New
class MyWidget(lv.buttonmatrix):

8. Button States

# Old (LVGL 7.x)
btn.set_state(lv.btn.STATE.INA)

# New (LVGL 9.x)
btn.add_state(lv.STATE.DISABLED)
btn.clear_state(lv.STATE.DISABLED)
btn.has_state(lv.STATE.PRESSED)

9. Label Text Alignment

# Old (LVGL 7.x)
lbl.set_align(lv.label.ALIGN.CENTER)

# New (LVGL 9.x)
lbl.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0)

10. Label Long Mode

# Old (LVGL 7.x)
lbl.set_long_mode(lv.label.LONG.BREAK)

# New (LVGL 9.x)
lbl.set_long_mode(lv.label.LONG_MODE.WRAP)

11. Object Deletion

# Old (LVGL 7.x)
obj.del_async()

# New (LVGL 9.x)
obj.delete_async()

12. Input Device

# Old (LVGL 7.x)
indev = lv.indev_get_act()
lv.indev_get_point(indev, point)

# New (LVGL 9.x)
indev = lv.indev_active()
indev.get_point(point)

13. Fonts

# Old (LVGL 7.x)
lv.font_roboto_22
lv.font_roboto_28
lv.font_roboto_16

# New (LVGL 9.x) - check available fonts
lv.font_montserrat_22
lv.font_montserrat_28
lv.font_montserrat_16
# Also available: lv.font_roboto_mono_*

14. Transparent/Invisible Styles

# Old (LVGL 7.x)
obj.set_style(lv.style_transp_tight)

# New (LVGL 9.x) - create manually
obj.set_style_bg_opa(0, 0)
obj.set_style_border_width(0, 0)
obj.set_style_pad_all(0, 0)

Display Initialization Order

Critical: In LVGL 9.x, display must be initialized before accessing themes/styles:

def init(dark=True):
    display.init()  # MUST be first
    init_styles(dark=dark)  # Now display_get_default() works

Common Pitfalls

  1. Forgetting style.init(): New styles must call init() before setting properties

  2. Wrong align function: align() is for parent-relative, align_to() is for sibling-relative

  3. Event registration mismatch: If you register for CLICKED, don't check for RELEASED in callback

  4. 5-arg vs 4-arg align: Old align had 5 args, new align_to has 4

  5. Missing display init: display_get_default() returns None if display not initialized

Testing Strategy

Use disco repl import to test module imports incrementally:

# Test specific module
scripts/disco repl import gui.components.keyboard

# Check available LVGL APIs
scripts/disco repl exec "import lvgl as lv; print([a for a in dir(lv) if 'theme' in a])"

# Check widget methods
scripts/disco repl exec "import lvgl as lv; btn = lv.button(lv.screen_active()); print(dir(btn))"

Files Migrated

  • gui/common.py - themes, styles, helpers
  • gui/core.py - display init, screen load
  • gui/async_gui.py - screen management
  • gui/decorators.py - event callbacks
  • gui/screens/screen.py - base screen class
  • gui/screens/menu.py - menu screen
  • gui/screens/prompt.py - confirmation dialog
  • gui/screens/alert.py - alert dialog
  • gui/screens/qralert.py - QR alert
  • gui/components/battery.py - battery indicator
  • gui/components/keyboard.py - keyboard widget
  • gui/components/qrcode.py - QR code display

Files Still Needing Migration

Many files still have .align( calls that need .align_to(:

  • gui/screens/transaction.py
  • gui/screens/mnemonic.py
  • gui/screens/input.py
  • gui/screens/settings.py
  • gui/screens/progress.py
  • gui/components/modal.py

Critical: QSPI Directory Shadowing Frozen Modules

Date: 2024-12-19

The Problem

After MicroPython/LVGL 9.3 upgrade, firmware fails to boot with:

ImportError: no module named 'hosts.Host'

Even though hosts is a frozen module, Python finds an empty /qspi/hosts directory first.

Root Cause Analysis

1. MicroPython's new module resolution

Old MicroPython (pre-upgrade) had absolute priority for frozen modules:

// Old: py/builtinimport.c - mp_import_stat_any()
mp_import_stat_t st = mp_frozen_stat(path);  // Check frozen FIRST
if (st != MP_IMPORT_STAT_NO_EXIST) {
    return st;  // Frozen always wins!
}
return mp_import_stat(path);  // Then filesystem

New MicroPython uses .frozen as a virtual sys.path entry:

// New: py/builtinimport.c
#define MP_FROZEN_PATH_PREFIX ".frozen/"
// Only checks frozen if path starts with ".frozen/"
// Priority depends on sys.path order

2. sys.path order

>>> import sys
>>> print(sys.path)
['', '.frozen', '/qspi', '/qspi/lib', '/flash', '/flash/lib']

The empty string '' = current working directory.

3. Default CWD is /qspi

>>> import os
>>> print(os.getcwd())
'/qspi'

MicroPython sets initial CWD to /qspi (the QSPI flash mount point).

4. The chain of failure

  1. sys.path[0] is '' (current directory)
  2. CWD is /qspi
  3. App creates /qspi/hosts/ for settings storage (via Host.SETTINGS_DIR)
  4. When importing hosts:
    • Python checks '' → resolves to /qspi → finds /qspi/hosts/ directory
    • Treats it as a package (directory = package in Python)
    • /qspi/hosts/ is empty (no __init__.py), so hosts.Host fails
    • Never reaches .frozen where real hosts module lives

5. Why old firmware worked

Old MicroPython checked frozen modules FIRST, regardless of sys.path order. The /qspi/hosts directory was ignored because frozen hosts was found first.

Potential Fixes

Option A: Change CWD in boot.py (Attempted)

# boot/debug/boot.py
os.chdir("/flash")  # Move away from /qspi
  • Pros: Simple, doesn't change firmware code
  • Cons: Still fragile if something else changes CWD back
  • Status: Tried but display still doesn't init (may be unrelated issue)

Option B: Rename settings directories

# src/main.py - change these:
Host.SETTINGS_DIR = platform.fpath("/qspi/hosts")      # OLD
Host.SETTINGS_DIR = platform.fpath("/qspi/settings/hosts")  # NEW

# src/helpers.py - change:
app = mod.App(platform.fpath("/qspi/%s" % modname))           # OLD
app = mod.App(platform.fpath("/qspi/settings/%s" % modname))  # NEW
  • Pros: Clean separation of data from module namespace
  • Cons: Requires migrating existing user data

Option C: Remove /qspi from sys.path

In f469-disco/micropython/ports/stm32/main.c:

// Comment out:
// mp_obj_list_append(mp_sys_path, mp_obj_new_str_via_qstr("/qspi", 5));
// mp_obj_list_append(mp_sys_path, mp_obj_new_str_via_qstr("/qspi/lib", 8));
  • Pros: Eliminates shadowing entirely
  • Cons: Users can't add custom modules to QSPI (rarely used feature)
  • Note: Requires C rebuild, may break QSTR extraction (see below)

Option D: Move .frozen before '' in sys.path

Modify f469-disco/micropython/py/runtime.c to insert .frozen at index 0 instead of appending:

// Instead of append, insert at position 0
mp_obj_list_insert(mp_sys_path, 0, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen));
  • Pros: Frozen modules always win
  • Cons: Requires MicroPython core modification

Related Issue: QSTR Build Failure After Clean

When running make clean && make debug, build fails with:

error: 'MP_QSTR_digest' undeclared
error: 'MP_QSTR_sha256' undeclared

Cause: uhashlib/hashlib.c wraps code in #if MODULE_HASHLIB_ENABLED. During QSTR extraction (preprocessing), this macro isn't defined, so QSTRs inside aren't collected.

Fix: Add to f469-disco/usermods/uhashlib/micropython.mk:

CFLAGS_USERMOD += -DMODULE_HASHLIB_ENABLED=1

Debugging Commands

# Check sys.path
scripts/disco repl exec "import sys; print(sys.path)"

# Check current working directory
scripts/disco repl exec "import os; print(os.getcwd())"

# Check what's shadowing
scripts/disco repl exec "import os; print(os.listdir('/qspi'))"

# Check hosts module path
scripts/disco repl exec "import hosts; print(hosts.__path__)"

# Remove shadow directories
scripts/disco repl exec "import os; os.rmdir('/qspi/hosts')"

# Test import after fix
scripts/disco repl exec "from hosts import QRHost; print('OK')"

Summary

The upgrade to new MicroPython changed how frozen modules are prioritized. Combined with:

  1. CWD defaulting to /qspi
  2. Settings directories using names that match frozen module names
  3. Empty string '' in sys.path meaning "current directory"

This creates a perfect storm where settings directories shadow frozen modules.

Implemented fix: Option D - Modified f469-disco/micropython/py/runtime.c to append .frozen before '' in sys.path. This ensures frozen modules always take priority regardless of CWD or filesystem directories.

// SPECTER: Put .frozen FIRST so frozen modules take priority over
// filesystem dirs that may shadow them (e.g. /qspi/hosts shadows hosts module)
#if MICROPY_MODULE_FROZEN
mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen));
#endif
mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR_)); // current dir

Result: sys.path = ['.frozen', '', '/qspi', ...] - frozen modules always win.

All Fixes Tested (2024-12-19)

Fix Description Result Notes
A Change CWD in boot.py ❌ FAILED Other dirs (e.g. /flash/keystore) also shadow
B Rename settings to /qspi/settings/* ✅ WORKS Requires migration of existing data
C Remove /qspi from sys.path ❌ FAILED '' (CWD) still resolves to /qspi
D Move .frozen first in sys.path ✅ WORKS Clean fix, no migration needed

Recommended: Fix D - minimal code change, no user data migration required.


VFS Filesystems Not Mounting

Date: 2024-12-19 Issue: specter-diy.new-dz2

After MicroPython upgrade, /flash and /qspi filesystems don't appear in sys.path:

>>> import sys
>>> print(sys.path)
['', '.frozen']  # Missing: /qspi, /flash, etc.

>>> import os
>>> os.listdir('/flash')
OSError: [Errno 19] ENODEV

Frozen modules work, but persistent storage unavailable. Needs investigation.

Root cause found: sdram.RAMDevice.ioctl() returns None instead of 0 for INIT/ERASE operations. New MicroPython's VfsFat.mkfs() is stricter.

Fix: f469-disco/usermods/sdram/sdram.c - return 0 for ioctl ops 1,2,3,6:

}else if(op_int == 1 || op_int == 2 || op_int == 3 || op_int == 6){
    // MP_BLOCKDEV_IOCTL_INIT, DEINIT, SYNC, BLOCK_ERASE - return 0 (success)
    return mp_obj_new_int(0);
}

STATIC Macro Removed

Date: 2024-12-19

New MicroPython removed the STATIC macro. All usermods must use static directly.

Affected files in f469-disco/usermods/:

  • scard/connection.c, reader.c, scard.c
  • sdram/sdram.c
  • udisplay_f469/display.c, display_unix.c
  • uebmit/uembit.c
  • uhashlib/hashlib.c, uhmac.c

Fix: Replace ^STATIC with static in all affected files.

SPI Flash Hang During Settings Load - 2025-12-19

Problem

Board hangs at "Loading settings..." during boot. REPL unresponsive.

GDB Backtrace

#0  0x080dce1e in spiflash_readblocks_raw at ports/stm32/spiflash.c:186
#1  0x080dce50 in spiflash_ioctl at ports/stm32/spiflash.c:216

PC confirmed: 0x080dce1e

Root Cause Analysis

Call Chain

spi_bdev_readblocks_raw (spibdev.c:92)
  -> mp_spiflash_read (spiflash.c:317)
    -> mp_spiflash_read_data (spiflash.c:148)
      -> mp_spiflash_transfer_cmd_addr_data (spiflash.c:109)
        -> qspi_read_cmd_qaddr_qdata (qspi.c:387)

Hang Location

In qspi_read_cmd_qaddr_qdata (qspi.c:423-428):

while (len >= 4) {
    while (!(QUADSPI->SR & QUADSPI_SR_FTF)) {  // <-- STUCK HERE
        if (QUADSPI->SR & QUADSPI_SR_TEF) {
            return -MP_EIO;
        }
    }
    *(uint32_t *)dest = QUADSPI->DR;
    ...
}

The code spins waiting for FIFO Threshold Flag (FTF) but it never gets set. Transfer Error Flag (TEF) is also not set, so no error exit.

Hypotheses

  1. QSPI peripheral not properly initialized
  2. Flash chip not responding (hardware issue)
  3. Flash chip in wrong mode (sleep/wrong QE bit)
  4. Clock/timing issue with QSPI prescaler

Next Steps

  • Check QUADSPI->SR register value via GDB
  • Verify flash chip JEDEC ID readable
  • Check if flash was left in deep sleep mode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment