Reverse engineering and clean-room rewrite of the firmware for the FNIRSI 2C53T, a 3-in-1 handheld oscilloscope/multimeter/signal generator. The original firmware was decompiled from binary using Ghidra and is being refactored into clean, modular C.
- MCU: Artery AT32F403A — ARM Cortex-M4F @ 240MHz, 1MB flash, 224KB SRAM (with EOPB0=0xFE)
- Originally identified as GD32F307 from firmware analysis; physical teardown revealed AT32 (markings sanded off)
- Register-compatible with GD32/STM32F1 at the GPIO/EXMC level
- LCD: ST7789V 320x240 RGB565 via 16-bit parallel EXMC/XMC bus
- FPGA: Gowin GW1N-UV2 (non-volatile, retains bitstream across power cycles) — handles 250MS/s ADC sampling via SPI3 data + USART2 commands
- SPI Flash: Winbond W25Q128JVSQ (16MB) — UI assets and system files
- DAC: 2-channel 12-bit (built-in) for signal generator output
- Buttons: 15 physical buttons — 4x3 bidirectional GPIO matrix (PA7/PB0/PC5/PE2 × PA8/PC10/PE3) + 3 passive (PC8=POWER, PB7=PRM, PC13=UP). 15/15 hardware-confirmed. TMR3 ISR at 500Hz.
- Touch: I2C interface (likely GT911/GT915) — not used for button input
- Board revision: 2C53T-V1.4
| Function | Pin | Notes |
|---|---|---|
| Power hold | PC9 | Must be HIGH immediately at boot or device shuts off |
| LCD backlight | PB8 | HIGH to enable |
| FPGA SPI3 data | PB3 (SCK), PB4 (MISO), PB5 (MOSI) | Bulk ADC data from FPGA (JTAG pins, remapped) |
| FPGA SPI3 CS | PB6 (GPIO) | Software chip select for FPGA SPI3 |
| FPGA SPI enable | PC6 (GPIO) | Must be HIGH for FPGA SPI3 to work |
| FPGA active mode | PB11 (GPIO) | Set HIGH during active measurement mode |
| FPGA USART cmd | PA2 (TX), PA3 (RX) | 9600 baud, 10-byte TX / 12-byte data RX / 10-byte echo RX |
| SWD | PA13 (SWDIO), PA14 (SWCLK) | Debug header near USB-C port |
| Battery sense | PB1 (ADC1 Ch9) | 239.5-cycle sample time, software-triggered |
| UART debug | RX, TX, GND | Through-hole pads (not yet mapped to MCU pins) |
| FPGA programming | M0-M3, GND, VDD, VPP | Header for Gowin programmer |
| Pinhole reset | NRST | Accessible from outside case |
firmware/ # Custom replacement firmware (GCC + Make)
src/main.c # FreeRTOS tasks, LCD init, UI event loop, mode switching
src/drivers/ # lcd.c / lcd.h — ST7789V driver via EXMC
include/ # FreeRTOSConfig.h
FreeRTOS/ # FreeRTOS kernel (submodule)
at32f403a_lib/ # Artery AT32 HAL library (clone from ArteryTek GitHub)
gd32f30x_lib/ # GD32 HAL library (legacy, kept for emulator builds)
bootloader/ # USB HID IAP bootloader (16KB at 0x08000000)
ld/ # Linker scripts (at32f403a_app.ld for hardware, at32f403a.ld for emu)
Makefile # Build: make, make flash, make flash-all, make emu
Makefile.hwtest # Minimal hardware test build
reverse_engineering/ # Ghidra decompilation artifacts
COVERAGE.md # RE coverage tracker: 309 real functions, fully catalogued
analysis_v120/ # Latest analysis: full_decompile.c, hardware_map, xref_map, RAM map, FPGA protocol
fpga_task_annotated.c # Annotated FPGA task (10 sub-functions, 580+ lines)
FPGA_TASK_ANALYSIS.md # FPGA protocol analysis: SPI3 format, command table, state machine
function_names.md # Complete function naming inventory (309 real + 61 false positives)
gap_functions.md # 17 gap functions catalogued with priorities
function_map_complete.txt # Complete 309-entry function map
decompiled_2C53T.c # V1.2.0 decompilation (~35K lines, 292+ functions)
decompiled_2C53T_v2.c # Updated with named functions (~39K lines)
strings_with_addresses.txt # 290 strings mapped to firmware addresses
ghidra_scripts/ # 14 Java automation scripts
emulator/ # Simulation infrastructure
renode/ # Full-system emulation (GD32F307 platform + peripherals)
renode_lcd_bridge.py # WebSocket bridge: Renode framebuffer → React frontend
emu_2c53t.py # Unicorn-based emulator (limited — no NVIC/SysTick)
frontend/ # React web UI for LCD display simulation (Vite)
docs/ # 33 design/analysis/planning documents (see docs/README.md for full index)
ghidra_project/ # Pre-analyzed Ghidra database (V1.2.0)
modules/ # JSON procedure files (automotive, HVAC, ham radio, education)
APP_2C53T_*.bin # Original firmware binaries (V1.0.3, V1.0.7)
FNIRSI_1013D_Firmware/ # Submodule: pecostm32's 1013D replacement firmware
FNIRSI_1014D_Firmware/ # Submodule: 1014D replacement firmware
FNIRSI-1013D-1014D-Hack/ # Submodule: historical RE work
cd firmware && make # Build for real hardware (AT32 @ 240MHz)
make flash # Flash via USB HID bootloader (case closed)
make flash-all # Flash bootloader + app via DFU (first-time setup)
make flash-dfu # Flash app via ROM DFU (fallback, BOOT0 + reset)- On device: Settings → Firmware Update (shows "BOOTLOADER MODE" screen)
- On host:
cd firmware && make flash - Device auto-reboots into updated firmware
Two bootloader modes exist — don't confuse them:
- ROM DFU (BOOT0 + pinhole reset, LCD dark,
2e3c:df11): only mode that can write option bytes. Required once. - HID bootloader (Settings → Firmware Update, "BOOTLOADER MODE" LCD): used by
make flashfor all normal updates. Cannot write option bytes.
- Enter ROM DFU (BOOT0 jumper to 3V3, pinhole reset). Verify:
dfu-util -lshows2e3c:df11with alt 0 and alt 1. - Set EOPB0=0xFE (224KB SRAM):
dfu-util -a 1 -d 2e3c:df11 -s 0x1FFFF800 -D firmware/build/option_bytes48.bin(48 bytes, generated bymake; the region size is literally 48 bytes per the DFU descriptor01*048 e— a shorter file gets rejected with "address out of range"). - Flash bootloader + app:
make flash-all - Close the case — all future updates go over USB-C.
0x08000000 Bootloader (16KB) — permanent, USB HID IAP
0x08003800 Upgrade flag (2KB sector)
0x08004000 Application (1008KB) — updated via make flash
make emu # Build for emulator (skips AT32 clock init)
make renode # Run in Renode (display-only)
make renode-interactive # Run with keyboard input (use with lcd_viewer)
make renode-test # 5-second smoke testToolchain: arm-none-eabi-gcc via Homebrew cask gcc-arm-embedded (native ARM64)
DFU tool: dfu-util via brew install dfu-util
Renode: Expected at /Applications/Renode.app
SDL3 viewer: cd emulator && make (requires brew install sdl3)
Logic analyzer: sigrok-cli via brew install sigrok-cli — drives HiLetgo 24MHz 8CH USB analyzer (fx2lafw driver)
- RTOS: FreeRTOS with Cortex-M4 port (240MHz tick, 1000Hz, 32KB heap)
- Original firmware tasks: Display, Input, Acquisition, Measurement, USB, FPGA
- Current custom firmware: Display task + Input task, 4 UI modes (scope, meter, signal gen, settings)
- LCD interface: Memory-mapped at 0x6001FFFE (command) / 0x60020000 (data), address line A17 selects RS/DCX
- EXMC config: SNCTL0=0x5011, SNTCFG0=0x02020424, SNWTCFG0=0x00000202 (works at 240MHz)
- Font system: Variable-width bitmap fonts at 4 sizes (12/16/24/48px), generated from TTF via
scripts/generate_font.py - Theme system: 4 color themes (Dark Blue, Classic Green, High Contrast, Night Red), switchable in Settings > Display Mode
- Emulator display: SDL3 native viewer reads
/tmp/openscope_fb.binat 30fps; interactive GPIO via/tmp/openscope_buttons.txt
- Target is Artery AT32F403A (STM32F1-compatible registers for GPIO/EXMC, but uses AT32 HAL for clock/peripheral init)
- IOMUX (AFIO) clock MUST be enabled for EXMC alternate function pins to work
- Power hold (PC9 HIGH) must be the very first operation in main()
- All display rendering is RGB565 (16-bit color)
- Firmware binaries are raw ARM code, not encrypted or compressed
- The decompiled source uses Ghidra naming conventions (FUN_, DAT_, etc.) — rename as functions are understood
- GPL v3 license
- Coverage: 309 real functions fully decompiled and named (138 HIGH, 182 MEDIUM, 42 LOW confidence). 61 Ghidra false positives eliminated.
- FPGA interface: Dual-channel — SPI3 (60MHz) for bulk ADC data, USART2 (9600 baud) for commands. Fully annotated.
- FPGA task: 10 sub-functions across 11.5KB, annotated in
analysis_v120/fpga_task_annotated.c - SPI3 data format: Interleaved CH1/CH2 unsigned 8-bit. Normal=1024B (512 pairs), dual=2048B. ADC offset=-28.0.
- SPI3 config: Mode 3 (CPOL=1, CPHA=1), Master, /2 clock (60MHz), 8-bit, software CS on PB6
- USART protocol: 10-byte TX frames (header + cmd + params + checksum). RX is two distinct frame types on the same stream: 12-byte data frames (header
0x5A 0xA5, carry BCD digits + status) and 10-byte echo frames (header0xAA 0x55, FPGA acks every TX command; byte[3] echoes our cmd, byte[7] = fixed0xAAintegrity marker). Timer-driven via TMR3. Seeanalysis_v120/usart2_isr_state_machine.md. - FPGA command codes: ALL ~40 mapped (0x00-0x2C) — scope, trigger, timebase, meter, siggen, freq counter, period, duty cycle, continuity/diode. Dispatch table at 0x0804BE74.
- FPGA control pins: PC6 = SPI enable (HIGH), PB11 = active mode (HIGH), PC11 = meter MUX enable (HIGH in meter mode)
- Analog frontend relays: PC12 = input routing, PE4/PE5/PE6 = range/attenuation select. DCV pattern: PC12=H, PE4=H, PE5=L, PE6=H
- Gain resistors: PA15, PA10 = gain select (HIGH for DCV), PB10 = gain select (LOW for DCV). Controlled by gpio_mux_porta_portb (FUN_08001A58)
- Additional frontend pins: PB9, PA6 = analog frontend control (undocumented, configured as outputs in stock init)
- SPI3 bulk exchange (cmds 0x3B/0x3A) — RESOLVED 2026-04-04, EXTRACTED + ANALYZED 2026-04-04: Stock master init runs a bulk SPI3 transfer during the FPGA init phase, opened with opcode 0x3B, closed with opcode 0x3A. Source table lives at
0x08051D19in the V1.2.0 binary. Transfer size is 115,638 bytes (38,546 × 3-byte records) — the prior "411 bytes" folklore was wrong. Ghidra had "misdecoded" the disasm region 4798–4833 as instructions; hand-decoding showed those 56 bytes are actually ASCII FreeRTOS task name strings (Timer1,Timer2,display,key,osc,fpga,dvom_TX,dvom_RX) embedded in the code stream with ab.nbranch jumping over them. There is no early-exit; the loop runs the full 115,638 bytes. The table has a 160-byte block structure withFF FF FF FF FF FFsentinels at byte 30 of each block — characteristic of a register-init table, not an FPGA bitstream. 2026-04-04 extraction + structural audit (viascripts/analyze_h2_table.py→analysis_v120/h2_extracted/): independently verified every top-line stat to the byte, and surfaced new findings — (1) the table has two clean regimes, not a gradient: Region A at0x00000–0x153FF(87,040 B, 544 sync-framed blocks) and Region B at0x15400–end(28,598 B, dense coefficients, no sync words); (2) every sentinel block has a 16-bit "tag" at bytes 28–29 immediately before the sync word, 269 unique values, non-monotonic — probably a per-block checksum (replay must be byte-exact); (3) post-sentinel bytes 36–38 are uniformly00 00 00= structural sync-confirm marker; (4) the last 8 blocks of Region A (536–543) share a00 65 c5terminator tag. Remains the leading hypothesis for missing factory calibration (low-Ω, DCV >10V). Our firmware does NOT replay this exchange — replay strategy for next session is a graduated 3-step experiment: Region A only (safest, clean boundary at0x15400) → full table → bench-capture fallback. Seeanalysis_v120/h2_extracted/FINDINGS.md(synthesis),analysis_v120/h2_extracted/h2_cal_table_analysis.md(numeric audit),analysis_v120/spi3_bulk_cal_resolved.md(original 5-agent analysis),analysis_v120/fpga_h2_spi3_bulk.md. - Master init decompilation: 4 phase files in analysis_v120/master_init_phase[1-4].c (~3500 lines total)
- Buttons: 15/15 HARDWARE CONFIRMED. Bidirectional 4x3 matrix + 3 passive. See
analysis_v120/button_map_confirmed.mdfor complete mapping. PRM (PB7) root cause was pull-up config (fromfulltest2.c) causing idle HIGH; fixed to pull-down inbutton_scan.c:button_scan_init. Bench-confirmed 2026-04-06. - Acquisition: Double-buffered (2 queue items per trigger), 9-mode state machine (fast TB, roll, normal, dual, extended, meter ADC, siggen, calibration, self-test)
- Calibration: ADC offset -28.0, per-channel VFP pipeline. Scope uses a 120-byte gain/offset cal table at RAM 0x20000358 (6 entries × 20 bytes, loaded from SPI flash at boot), indexed by voltage range. FUN_080018A4 (
gpio_mux_portc_porte) reads this table to compute the scope trigger comparator DAC1 value — it's a SCOPE trigger path, not meter cal. The 301-byte regions at state[0x356]/state[0x483] are scope roll-mode sample buffers, NOT calibration data. Meter cal source is still unresolved — the leading hypothesis is the FPGA-side 411-byte cal exchange via cmds 0x3B/0x3A at boot. See analysis_v120/cal_data_myth_busted.md for the full hypothesis history and H1 postmortem. - Meter data: BCD digit extraction from cross-byte nibbles in USART RX frames, 8-state mode FSM. Frame layout: 12-byte data frames (header
0x5A 0xA5) and 10-byte echo frames (header0xAA 0x55, byte[3]=echoed cmd, byte[7]=0xAAintegrity marker). Seeanalysis_v120/usart2_isr_state_machine.md. - Meter DAC reference calibration: Per-range DAC value computed by
FUN_080018A4using meter cal tables at0x20000394(upper bounds) and0x20000358(baselines), each 2 bytes × N entries (~40 bytes total). Formula:DAC = ((*puVar1 - *puVar2) / divisor) * (offset + 100) + *puVar2, written to0x40007408. TheCLAUDE.mdclaim about "301-byte per-channel meter cal data loaded from SPI flash" was wrong — that 301-byte region is the scope roll-mode sample buffer, not meter cal. Seeanalysis_v120/cal_data_myth_busted.md. - Boot sequence: 53-step init documented in
reverse_engineering/analysis_v120/FPGA_BOOT_SEQUENCE.md - Master init: FUN_08023A50 (15.4KB) — configures all peripherals, creates all FreeRTOS tasks
- 8 FreeRTOS tasks: display, key, osc, fpga, dvom_TX, dvom_RX, Timer1, Timer2
- 7 FreeRTOS queues: usart_cmd (0x20002D6C), button_event (0x20002D70), usart_tx (0x20002D74), spi3_data (0x20002D78), meter_sem (0x20002D7C), fpga_sem1 (0x20002D80), fpga_sem2 (0x20002D84)
- Auto power-off: 3 tiers (15min/30min/1hr) based on probe state
- Watchdog: IWDG fed every 11 calls to input_and_housekeeping (~50ms)
- Calibration tables in RAM: 120-byte scope gain/offset cal at 0x20000358 (6 entries × 20 bytes, loaded from SPI flash at boot). Used by scope_main_fsm (indexed by voltage range) and FUN_080018A4 (
gpio_mux_portc_porte) to compute the scope trigger comparator DAC1 value. Not a meter cal source. - Filesystem paths in firmware:
2:/Screenshot file/,3:/System file/ - Firmware versions analyzed: V1.0.3 → V1.0.7 → V1.1.2 → V1.2.0
- IOMUX remap:
(reg & ~0xF000) | 0x2000at AFIO+0x08 — disables JTAG-DP, keeps SW-DP, frees PB3/PB4/PB5 for SPI3 - Battery ADC: PB1 / ADC1 Channel 9, 239.5-cycle sample, right-aligned, software-triggered
- TMR8: Actually IS configured (ARR=99, generates periodic interrupt for FatFs). Not unused as previously thought.
- DMA: Ch1 = LCD framebuffer (16-bit mem-to-mem → EXMC). SPI3 = polled. USART2 = interrupt-driven.
- Key docs:
reverse_engineering/ARCHITECTURE.md(start here),FPGA_PROTOCOL_COMPLETE.md,HARDWARE_PINOUT.md,CALIBRATION.md,analysis_v120/FPGA_TASK_ANALYSIS.md
- Full oscilloscope UI running on real FNIRSI 2C53T hardware (AT32F403A @ 240MHz, battery powered)
- LCD driver functional with multi-size font system (4 sizes from SF Pro + Menlo)
- 4 themed UI modes: oscilloscope (with FFT/waterfall), multimeter (large digits), signal generator, navigable settings
- Theme switching (4 themes) wired through all screens
- FreeRTOS scheduler running with display + input tasks
- Power management: PC9 hold, PB8 backlight, POWER button 3-2-1 countdown shutdown, low-battery auto-off at 3.3V
- USB HID bootloader — closed-case firmware updates via
make flash, LCD status screen, POWER button exit, auto-reboot after flash - Battery monitor: PB1 ADC with 16-sample averaging, percentage display, USB charge detection ("CHG"), LiPo protection shutdown
- SDL3 native LCD viewer with interactive button input for emulator
- Soak testing infrastructure (random button fuzzing with fault monitoring)
- Watchdog, health monitoring, task stack checking
- Button input: 15/15 HARDWARE CONFIRMED — bidirectional 4x3 matrix scan at 500Hz via TMR3 ISR. Rows: PA7,PB0,PC5,PE2. Cols: PA8,PC10,PE3. Passive: PC8(POWER),PB7(PRM),PC13(UP). Complete mapping in
analysis_v120/button_map_confirmed.md. PRM pull-down fix bench-confirmed 2026-04-06. - FPGA USART communication working — bidirectional, frames captured, meter data flowing
- Meter reading pipeline working — first live readings 2026-04-03. The original "3.7x high" problem turned out to be a decimal-point placement bug, not a gain error; fixed once the frame[6] decoder was wired up. DCV 0–9V and the full resistance range are now accurate within a few percent. See
analysis_v120/meter_math_pipeline_annotated.cfor the full pipeline trace andsrc/drivers/meter_data.cfor the current decoder. - Meter low-Ω and kΩ band override (2026-04-04): Before tonight the display flickered between correct and wrong readings on the same resistor (e.g., a 10kΩ probe alternated between
9.821 kOhm✓ and98.24 kOhm✗). Root cause: the FPGA meter IC rotates through severalframe[6]byte variants per measurement (0x07, 0x0A, 0x0B, 0x0D, 0x0E, 0x0Ffor low-Ω;0x40, 0x4A, 0x4B, 0x4D, 0x4Efor kΩ), each claiming a different dp interpretation of the same raw BCD. Fix:meter_data.cnow computes the resistance fromraw_bcddirectly at the band level, ignoring the per-frame dp hint. Upper nibble 0 of frame[6] → low-Ω regime →ohms = raw_bcd × 0.0304. Upper nibble 4 → kΩ regime →kohms = raw_bcd × 0.001. Result is stable regardless of which frame variant arrives first. Factor 0.0304 is per-device (factory-calibrated); currently hardcoded for bench unit #1. Seeanalysis_v120/meter_math_pipeline_annotated.c. - Meter DCV >10V is a known limitation (2026-04-04): Bench captures show 7V reads correctly (f6 stable at
0x07, raw 7005, dp=1) but 11V reads as0.997V(f6 rotates through0x0A, 0x0F, 0x0B, 0x07, raw=997, wrong dp latch). Same root cause as low-Ω: the meter IC's auto-range is unstable without factory cal. Candidate fixes filed inmeter_data.c: (1) systematic bench capture across voltages, (2) firmware-driven range select via FPGA commands, (3) H2 SPI3 bulk cal replay experiment. Deferred to a future session. - Boot safety system — 3-strike crash recovery: app crashes 3x → bootloader enters SAFE MODE automatically. Hold POWER at boot = force bootloader. boot_validate() handshake. Never need to open case again.
- Fuse current tester UI — 4th meter layout (OK cycles Full→Chart→Stats→Fuse). 3 views, 5 fuse types, 47 ratings. Uses real meter mV readings for parasitic drain estimation.
- FPGA SPI3 root cause identified — was missing: PB11 HIGH (active mode), full USART boot command sequence (0x01-0x08), queue-driven triggering (not polled), SysTick delays between boot phases. See
FPGA_TASK_ANALYSIS.md - Stock firmware ~98% understood — 309 functions named, ALL FPGA commands mapped, ADC format cracked, button input resolved, battery ADC found, IOMUX remap extracted. Master init fully decompiled in 4 phase files. Remaining: dispatch table mechanism (null entries), 42 low-confidence function names.
- Meter poll decoupled from UI (2026-04-04): The FPGA meter IC only emits data frames in response to recent TX commands. This was originally worked around by calling
fpga_send_cmd(0x00, 0x09)insidedraw_meter_screen(), which coupled the data cadence to the UI redraw rate. Now handled by a dedicatedfpga_meter_poll_taskinfpga.cthat polls at ~4 Hz whilecurrent_mode == MODE_MULTIMETER, regardless of UI activity. Reduces USART traffic ~7.5x and decouples data flow from draw scheduling. Seeanalysis_v120/usart2_isr_state_machine.md. - Meter redraw throttled to data-change events (2026-04-04):
main.cwas unconditionally callingdraw_meter_screen()every 50ms (20 Hz), which caused visible flicker from the full-arealcd_fill_rect→ redraw sequence. Now gated onmeter_reading.update_countchanging, with a 1-second safety tick. Drops redraws to the FPGA data rate (~4 Hz) and eliminates flicker. Scope and signal gen still have the same structural issue (unconditional animation branches) — filed as a future "rendering pass" TODO that would introduce per-component dirty tracking (retained-mode UI à la React reconciliation). 224KB SRAM is plenty for component-level dirty state; no framebuffer needed.
A parallel five-agent deep-dive session produced five substantial analysis files and surfaced several corrections to prior claims. Deliverables in reverse_engineering/analysis_v120/:
-
meter_math_pipeline_annotated.c(1008 lines) — Full annotation ofdvom_rx_task(0x08036AC0, 1776B) andfpga_state_update(0x080028E0, 768B). BCD digit extraction from cross-byte nibbles, 8-state mode FSM, decimal scaling pipeline, frame[6] decoder table. The "3.7x high" bug turned out to be a decimal-point placement issue (already fixed). -
spi3_bulk_cal_resolved.md— Hand-decoded the init lines 4798–4833 that Ghidra had mis-labeled as instructions. Definitively resolved the SPI3 bulk exchange size: 115,638 bytes, not 411. Table structure suggests a register-init blob, not an FPGA bitstream. Remains the leading hypothesis for missing factory cal. -
scope_render_monsters_annotated.c(1535 lines) — Annotation of the "waveform render" monsters andscope_mode_cursor. Major correction:FUN_08030524(6632B) andFUN_08031f20(4110B), labeledwaveform_render_ch1/ch2infunction_names.md, are actually a JFIF/JPEG Huffman decoder (SOI markers, DHT tables, IDCT butterfly constants0x16A0,0x1D90,0x29CF). They decode boot-logo / UI assets from SPI flash. The real scope waveform draw is a filled-capsule (Bresenham circle end-caps + line segments) inFUN_08019470+FUN_08018DA0+FUN_08015F50, fixed color0xFB43(amber), no persistence. Y-transform formula:display_y = (scale_a/scale_b) × (raw − 128.0 − baseline) + 128.0 + dc_offset, clamped to[28, 228],scale_a/bfrom a 16-bit LUT at flash0x080465CC. Also:scope_mode_cursoris actually a measurement engine computing Vpp/Vmax/Vmin/Vavg/Vrms/frequency/period with 64-bit VFP accumulators — should be renamedscope_measurement_engine. -
stock_iap_bootloader.md— Annotation of USB and FMC functions. Major correction: stock firmware has no in-app programming path at all. USB is Mass Storage Class (BOT) on EP1 bulk with 12 SCSI commands (screenshot/file browser), not HID IAP.FUN_0802f3e4andFUN_0802f5ec, labeledfmc_program_flashandfmc_erase_pageinfunction_names.md, are actually option-byte writers (they set EOPB0=0xFE for the 224KB SRAM mode). Stock factory upgrades must have used ROM DFU or a separate bootloader binary not present inAPP_2C53T_V1.2.0. The "caseless bridge flash" idea is infeasible — new units need ROM DFU (BOOT0 + reset) or SWD once; after that our HID bootloader handles everything. -
reset_and_clock_init.md— Decoded the reset handler and clock init at0x08000000–0x08000238. 240MHz confirmed, but via a different PLL path than ours: stock usesHEXT ÷ 2 × 60 = 240MHz(pllhextdiv=1, pllmult_l=11, pllmult_h=3 → 11 + 16×3 + 1 = 60). Our firmware usesHEXT × 30 = 240MHz; both produce the same SYSCLK. Stock is built with Keil MDK (ARMCC), not GCC — explains the scatter-load layout, TBB jump tables, and absence of an explicitSystemInit(). No firmware changes needed.
Corrections to analysis_v120/function_names.md stemming from this session:
waveform_render_ch1/ch2→jpeg_huffman_decode_1/_2scope_mode_cursor→scope_measurement_enginefmc_program_flash→option_bytes_write_optkeyrfmc_erase_page→option_bytes_erase(companion)