You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Build debug firmware
make debug
Flash to board
scripts/disco flash program bin/debug.bin --addr 0x08000000
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
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'
Fix the issue in source code
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:
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
Mass erase deletes bootloader - must reflash from 0x08000000
QSPI shadow dirs can brick device - even with correct firmware
Flash verify passes but device crashes - issue is in QSPI, not internal flash
Keep full firmware images in fwbox/old/main/ for recovery
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
Flash spflashbug/debug.bin via scripts/disco flash program --addr 0x08000000
Verify with disco flash verify --smart → PASSED
Flash same file via Finder (drag to DIS_F469NI drive)
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
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 accessth.style.btn.rel.body.main_color=colorlv.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_* methodsstyle=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 selectorobj.add_style(style, lv.STATE.PRESSED) # for pressed state
7. Widgets Removed/Changed
# lv.page removed - use lv.obj with scrolling# Oldself.page=lv.page(parent)
# New - regular obj is scrollable by defaultself.page=lv.obj(parent)
# lv.btn renamed# Oldbtn=lv.btn(parent)
# Newbtn=lv.button(parent)
# lv.btnm renamed# OldclassMyWidget(lv.btnm):
# NewclassMyWidget(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_22lv.font_roboto_28lv.font_roboto_16# New (LVGL 9.x) - check available fontslv.font_montserrat_22lv.font_montserrat_28lv.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 manuallyobj.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:
definit(dark=True):
display.init() # MUST be firstinit_styles(dark=dark) # Now display_get_default() works
Common Pitfalls
Forgetting style.init(): New styles must call init() before setting properties
Wrong align function: align() is for parent-relative, align_to() is for sibling-relative
Event registration mismatch: If you register for CLICKED, don't check for RELEASED in callback
5-arg vs 4-arg align: Old align had 5 args, new align_to has 4
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:
New MicroPython uses .frozen as a virtual sys.path entry:
// New: py/builtinimport.c#defineMP_FROZEN_PATH_PREFIX ".frozen/"
// Only checks frozen if path starts with ".frozen/"// Priority depends on sys.path order
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.pyos.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)
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:
The upgrade to new MicroPython changed how frozen modules are prioritized. Combined with:
CWD defaulting to /qspi
Settings directories using names that match frozen module names
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)#ifMICROPY_MODULE_FROZENmp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen));
#endifmp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR_)); // current dir