Target binary: mainware_1.07.06.bin (218784 bytes, ARM Cortex-M4,
STM32F413VGT6). Loaded into Ghidra at 0x08020000 so the 512-byte
envelope at file offset 0..0x1FF lands at the start of flash sector
5, and the vector table at file offset 0x200 lands at 0x08020200
— naturally 512-B-aligned for VTOR. Image spans sector 5 + part of
sector 6 (ends ~0x080556A0, ~213 KB).
See docs/hardware.md for the canonical binary identity, the envelope
format, the MCU identification work, and the version-history note that
picks 1.07.06 as the baseline.
Mainware is VanMoof's own application written by VanMoof (or
VanMoof contractors) on top of the Muco runtime + ST CubeF4 HAL. The
working policy is identical to mainboot:
- Translate every function that has observable behaviour
(skip 2-byte
b .trap stubs — they get listed asdecomp-asm, one shared source line). - Recognise ST CMSIS / HAL / LL stock functions when they
appear and mark them
vendor-stock. Mainware almost certainly links againstHAL_FLASH_*,HAL_UART_*,HAL_CRC_*,HAL_GPIO_*,HAL_TIM_*, and a chunk of CMSIS-Core intrinsics. The build pulls those from a vendored Cube tree later. - Recognise Muco runtime functions shared with
mainboot—systick_tick,scheduler_tick,systick_delay, the RCC-reset and CRC helpers. These also becomevendor-stockrather than getting re-decoded from scratch; same code, same expected names, just embedded in a different image. - Recognise libc (
memcpy,memset,strlen,strcmp,printffamily) — these are pulled in fromarm-none-eabi-newlibby the Cube build. Markvendor-stockand supply from the vendored libc.
The bespoke layer worth understanding deeply is the application layer: the super-loop / scheduler structure, the BLE / Modbus command tables, the power-state machine, the modem driver, the per-subsystem updater flows, and the bike-state model.
| Count | Status |
|---|---|
| 130 | pending (auto-named FUN_xxxxxxxx) — live Ghidra count; the bespoke application layer is now 100% sourced. The remainder is 116 vendor/stock-HAL (newlib/libgcc + CubeF4) + 14 phantom-already-sourced (Ghidra-split tails / externs whose OEM addr is cited in src). Verified by a 14-agent fan-out: 11/11 newly-decoded functions PASS against the raw disassembly and a 3-range census critic examined 137 FUN_ and found 0 missed bespoke ("could not falsify" the completeness claim). See "Remaining FUN_ triage". |
| 24 | vendor-stock — strcmp, strtol, strlen, snprintf, memcpy, memset, __libc_init_array, _init, __getreent, malloc, free (newlib), __floatsidf (libgcc); CubeF4 HAL: HAL_GPIO_WritePin, HAL_GPIO_Init, HAL_GPIO_ReadPin, HAL_FLASH_Program, HAL_FLASH_Unlock, HAL_FLASHEx_Erase, FLASH_WaitForLastOperation, HAL_CRC_Accumulate; CMSIS NVIC_DisableIRQ (0x080270FC, writes NVIC->ICER); memcmp, strstr, strchr, strncmp (newlib — the last named bounded_strncmp, returns 0 == equal). (The CubeF4 I²C HAL — HAL_I2C_Init/DeInit/Mem_Write/Mem_Read/Master_Transmit/Master_Receive — is no longer vendor-stock: reconstructed as faithful C in i2c.c, see the per-module log.) |
| 0 | in-progress |
| 519 | decomp-c — vectors.c (25), tim.c (9), stc3115.c (16), console_edit.c (12), systick.c (3), console.c (65), scheduler.c (8), exceptions.c (10), panic.c (2), app.c (25), util.c (6), system_stm32f413.c (1), ssp.c (22), motor.c (9), flash.c (9), crc.c (6), audio.c (1), log.c (10), sensor.c (9), uart.c (8), bus.c (3), net.c (2), gpio.c (5), eeprom.c (3), i2c.c (21), watchdog.c (6), states.c (7), modem.c (56), ble.c (5), ble_read.c (3), rtc.c (9), update.c (1), main.c (4), battery.c (29), display.c (33), lighting.c (9), display_requests.c (data: 38 descriptors), shifter.c (39), lis3dh.c (26), comm.c (2) — see per-module log below |
| 1 | decomp-asm — startup_stm32f413.S: Reset_Handler (+ vector table, envelope, Default_Handler, reset_region_empty_stub) |
| 134 | named (rename in Ghidra, no source yet) — (comm_buffers_register_all moved named→sourced this pass) — the STC3115 config/startup/algorithm layer named+prototyped this pass (extern, bodies deferred): stc3115_apply_config (0x080394DC, writes CC_CNF/VM_CNF/alarm regs + MODE), stc3115_startup_from_ocv (0x08039580), stc3115_startup_restore (0x080395A8), stc3115_init_device (0x080395D0, top init: sets cfg params, checks RAM sig 0x53A9 + CRC, branches restore-vs-init); the SOC-tracking task stc_read (0x080396E4), stc3115_fuel_gauge_init (0x08037130) and stc3115_wake (0x080398CE) remain named — the CubeF4 HAL TIM core named+prototyped this pass (extern in tim.c, bodies deferred): HAL_TIM_Base_Init (0x080277B0), HAL_TIM_PWM_Init (0x080277E4), TIM_Base_SetConfig (0x080276E8), HAL_TIM_ConfigClockSource (0x08026DC0), HAL_TIM_PWM_ConfigChannel (0x08027888), TIM_OC1/2/3/4_SetConfig (0x080273B4/0x08027818/0x0802741C/0x0802748C), HAL_TIMEx_ConfigBreakDeadTime (0x08026E48), TIM_CCxChannelCmd (0x08027964), HAL_TIM_PWM_DeInit (0x0802752A); plus the global HAL_MspInit (0x0803C270, PWR+SYSCFG clock enable) and i2c2_wait_bus_idle (0x0803C8A8, I2C2 SR2.BUSY 50 ms wait used by the display/light I2C path) — the 9 shared IRQ leaves the vector trampolines forward to, named this pass (extern in vectors.c, bodies decode-pending): rtc_wakeup_irq_handler (0x08027020), the CubeF4 HAL servicers HAL_DMA_IRQHandler (0x08022BDC), HAL_TIM_IRQHandler (0x0802756C), HAL_I2C_EV_IRQHandler (0x08025E04), HAL_I2C_ER_IRQHandler (0x08025F9E), and the four EXTI/TIM application pre-hooks exti4_app_hook (0x08043CEC), exti9_5_app_hook (0x08038FF4), tim6_app_hook (0x08037AA8), tim7_app_hook (0x08039138) — system_reset (0x08035CE8, AIRCR SYSRESETREQ) + console_io_table_install (0x080430D8, binds g_log_func to the UART7 console table) + usart1_io_table_install (0x0804309C, binds g_log_func to the USART1 console table) named as referenced leaves of the UART byte-pump cluster (bodies extern, not sourced; uart_rx_ringbuf_get_byte was sourced) — HAL_GPIO_EXTI_Callback (0x08038964, the application's EXTI dispatch override, named this pass; its ~150-byte body is fired by the now-sourced HAL_GPIO_EXTI_IRQHandler — left for a future pass) + the UART-init HAL externs HAL_RCC_GetPCLK2Freq (0x08027394), HAL_RCC_GetHCLKFreq (0x08027368), HAL_UART_MspInit (0x080333C0) named this pass (called from uart.c's reconstructed init path; HAL_UART_MspDeInit/HAL_RCC_GetPCLK1Freq were already named) + modem_sms_dispatch_command (0x0803D668, the inbound-SMS remote-command interpreter, named this pass) + 24 named leaves were SOURCED into their home modules this pass (see the "Named-leaf sourcing pass" log entry): the ble_read telemetry getters (ble_get_charge_plug_state/ble_build_testmode_versions_blob/charge_level_adc_get/ble_get_led_channel_state/telemetry_map_clamp/gpio_pc0_is_low/gpio_pc1_is_low/hw_version_lookup/maybe_enqueue_tx_message), the bike-state getters (bike_status_coarse_get/bike_state_is_standby/ble_lock_state_get/ble_unlock_state_get), the RTC helpers (rtc_now_epoch_seconds/rtc_set_from_unix_time → new rtc.c), app/state helpers (app_ctx_clear_field_328/clear_flag_00e5/count_active_2bit_groups/announce_records_reset/display_announce_enter/display_timeout_timer_set/lock_poll_timer_arm) and the BLE helpers (sspm5_tx_timeout_cb/post_request_with_arg). config_persist_dual_bank stays named (its OEM body memcpy's 0xC0 bytes from &stack0x00000000 — a variadic stack passthrough that doesn't model cleanly in portable C). The rest still pending. — (the LED-matrix display engine is now SOURCED → display.c (display_module_init/display_send_init_cmd/display_write_reg20_init/display_panel_reset/led_driver_panel_config/led_driver_brightness_write/led_driver_standby_write/led_driver_set·enter_shipping_mode/led_matrix_render·overlay_frame_region/led_matrix_transmit_step/matrix_draw_speed·number·icon·level_bar·level_bar_blink/matrix_set_corner_led·turn_indicator/matrix_glyph_src_addr·frame_delay/display_mode_sm_step/display_request_*/is_display_bus_ready/etc.) and the lamp engine + ambient sensor → lighting.c (light_pattern_step/light_pattern_action_apply/light_sensor_read_step·i2c_read·fault_count_get/obj_set_field34·38/led_channel3_set_brightness), with ~25 FUN_ helpers named this pass, fan-out transcribed + adversarially verified (docs/display.md, docs/lighting.md). Two name corrections: dsp_recovery_telemetry_pump→**led_matrix_transmit_step** ("dsp" in " ERR dsp freeze" = display, not the C28x DSP); and light_tick_update (0x080371E8) identified as the command-mode console↔modem/BLE-debug UART bridge (it shuttles bytes between the UART7 console and either USART2/GSM via gsmdebug or UART8/BLE-debug via bledebug), NOT lamps (still named, out of scope). — The battery/BMS Modbus driver — modbus_bat_submit/bms_modbus_read/bms_modbus_write/bat_modbus_master_step/bms_telemetry_unpack/battery_telemetry_step/battery_state_process/battery_charge_display_step/modbus_bat_service_step/etc. — is now SOURCED → battery.c; the shared bus CRC/TX helpers bus_crc16_get/update/reset/verify + bus_tx_enqueue_byte/_n were named this pass, extern pending a future bus.c. The spine main/boot_init_cold/boot_init_warm/mainware_boot_init_sequence and status_process are now SOURCED — status_process → states.c (the 62-case behaviour engine, adversarially verified), the rest → main.c; with their ~74 callees named this pass, docs/boot.md: peripheral init hal_mcu_init/dma_controller_init/usart1·2·3·6_init/uart5·5·7·8_init/i2c2_init/tim1_pwm_init/tim6·7·10_init/adc1_init/rtc_init/crc_init/comm_buffers_register_all, clock rcc_oscillator_config/rcc_clock_config/rcc_periph_clock_config/tim_channel_enable_output, loop services light_tick_update/light_pattern_step/modbus_shifter_link_monitor/lipo_charge_state_monitor/ssp_ble_tx_queue_pump/motor_fw_update_fsm_step/sspm_rx_reply_handler/sspm_tx_queue_pump/led_matrix_render·overlay_frame_region/dsp_recovery_telemetry_pump/charger_and_pc1_sense_debounce/supply_voltage_sample_step/output_value_filter_step/ble_telemetry_change_broadcast/update_sm_is_idle/log_upload_sm_step/display_mode_sm_step/factory_reset_sm_step/staged_msg_validate_and_dispatch/button_press_state_machines_step/app_ctx_ptr_set, boot devices display_module_init/hdc1080_write_config_reg/stc3115_wake/stc3115_fuel_gauge_init/lis3dh_accel_init/audio_amp_init/display_write_reg20_init/eeprom_read_id_block/eeprom_read_config_with_crc_fallback/flash_read_config_with_crc_restore/sound_groups_init_default/region_speed_preset_table_load/flash_program_rdp_level_once/reset_reason_log_and_clear/log_wake_reason/log_console_subsystem_init), OTA helpers (flash_cache_disable, flash_cache_enable, download_chunks_pending_count, shifter_update_status_get, shifter_update_request, batteryware_update_status_get, batteryware_update_set_pending, bus_rx_byte_locked), BLE (maybe_enqueue_tx_message), lock/alarm state (bike_is_locked, ble_lock_state_get, ble_unlock_state_get, bike_state_is_standby, bike_status_coarse_get), modem/tracking (modem_sim_state_machine, sms_info_tracking_state_machine), battery (modbus_bat_service_step, modbus_bat_submit, modbus_shift_submit, battery_request_telemetry, bms_modbus_read, console_battery_dump, stc_read, gas_gauge_reset, batteryware_update_arm), motor (motor_get_timer_cb), shifter (shifterstatus_dump_v200, shifterstatus_dump_v201), ADC (hw_version_lookup, adc_read_vgsm, adc_read_5vsw), console (console_cmd_show, console_cmd_ver), log (log_buffer_dump), flash/eeprom (config_persist_dual_bank, flash_config_bank_write, save_state_record_to_eeprom, settings_factory_reset, reboot_restart_task, bat_reset_release_cb), misc (testmode_command_dispatch, rtc_fill_time_fields), + 42 status_process per-state sub-handlers (status-process.md): shifter-SM steps (shifter_sm_get_step/set_step_3/_10/_13, shifter_get_active_flag, shifter_firmware_update_step), state_flags_set/clear/test (64-bit flag pair ctx+0x3B8), LIS3DH (lis3dh_int1_clear/powerdown/config_motion_int — SOURCED → lis3dh.c this pass; accel_enable→stc_gas_gauge_set_run), locked_state_step, power_assist_gear_step, diagnostics_run_step, internal_lipo_charge_step, enter_stop_mode, system_reset (NVIC), led_driver_set/enter_shipping_mode, light_sensor_read_step, charge_level_adc_get, battery_on_detect_step/substate_advance, bms_write_reg8_and_poll, telemetry_datalog_emit, sched_timer_arm_or_alloc, set_unlock_state_persist, sms_track_state_get, state_table_ptr_get, etc. |
function_count = 814 per ghidra/exports/mainware_program.json (3 OEM functions newly
created an earlier pass: console_cmd_shipping, shiftdebug_pump_task, bat_reset_release_cb;
motor_post_update_cb0x08030994created in the motor-update FSM pass — the dump must materialize it, sofunction_countis now 815. The USART2/UART8/UART7 byte-pump pass created one more function —console_printf0x080367F0(the previously-undefined g_log_func[0] printf) — so the dump must materialize it too, making it 816. The USART1 console-port pass createdusart1_printf0x08035EBC(the previously-undefined USART1 printf) → the dump must materialize it:function_countis now 817). The 49-command console dispatch table is mapped indocs/console.md— all 49 handlers decoded (47 sourced intoconsole.c,show+vernamed+documented). The committed JSON is stale: ~240 functions have been renamed + given prototypes/no-return across the recent sessions — incl. the shifter pass (23FUN_*renamed tobus_queue_*/shifter_*/modbus_frame_flushand the new functionshift_rx_flush_timeout_cb@0x08037410, which the dump must materialize) and the placeholder-decode pass (~45 leaves named, incl. the correctionsaccel_enable→stc_gas_gauge_set_runandflash_cache_disable/enable→wwdg_apb_clk_disable/enable) (everything in the Decoded, Decomp-asm, Vendor-stock and Named tables below carries its OEM address, so those tables are the authoritative name map until the JSON is regenerated). The GhidraMCP server can't run the dump script — re-runghidra/scripts/DumpMainwareProgram.javain Ghidra to refresh it. The program itself was saved after each session. (This pass renamed 36 more functions — the 32 modem AT callbacks0x0802F1DC..0x0802FD68,strstr/strchr/bounded_strncmp, andmodem_sms_dispatch_command— set 36 prototypes, and labelled the 13g_modem_*SRAM scratch globals +g_pModemProvision@0x0804F440.) The LIS3DH pass renamed 25 more (0x0803D070/074/098transport,0x0802E434/444/44Ethunks, the 15 register helpers0x0802E45E..0x0802E782,lis3dh_int1_read_source0x0803D1D4,charge_time_estimate_reset0x0802E40C,NVIC_DisableIRQ0x080270FC,HAL_I2C_Mem_Read0x08024E90), set 29 prototypes, labelledg_lis3dh_dev@0x2000838C+g_lis3dh_int1_last_src@0x200001E0, and added 2 plate comments — program saved. The I²C HAL pass renamed 15 more (the 10 transfer statics0x08023EEC..0x08024504,HAL_I2C_Master_Transmit0x08024760,HAL_I2C_Master_Receive0x080248D8, plus the 3 referenced externsHAL_RCC_GetPCLK1Freq0x08027374/HAL_I2C_MspInit0x0803C69C/HAL_I2C_MspDeInit0x0803C820) and set 12 prototypes — program saved. The UART HAL init pass renamed 6 more (HAL_UART_Init0x08026CFC,UART_SetConfig0x08026AF0,HAL_UART_DeInit0x08026D5A← wasuart_handle_deinit,HAL_RCC_GetPCLK2Freq0x08027394,HAL_RCC_GetHCLKFreq0x08027368,HAL_UART_MspInit0x080333C0) and set 6 prototypes — program saved. The GPIO HAL deinit/EXTI pass renamed 3 more (HAL_GPIO_DeInit0x08026990,HAL_GPIO_EXTI_IRQHandler0x08026AD4,HAL_GPIO_EXTI_Callback0x08038964) and set 3 prototypes — program saved. The SSPM-bus TX pass renamed 1 more (sspm_bus_send_byte0x0803662C← wasFUN_0803662C) and set 2 prototypes (sspm_bus_send_byte,sspm_bus_send_frame0x0803A008) — program saved. The motor-DSP download transport pass renamed 5 more (sspm_bus_get_byte0x08036664,motor_dl_send_buf_step0x08030B70,motor_dl_recv_buf_step0x080309C4,motor_dl_recv_byte_step0x08030A50,motor_dl_recv_u16_step0x08030ADC) and set 5 prototypes — program saved. The motor-DSP transaction-SM pass renamed 3 more (motor_dl_send_verify_step0x08030BFC,motor_dl_autobaud_step0x08030CEC,motor_dl_stream_block_step0x08030D88) and set 3 prototypes — program saved. The motor-update FSM pass sourcedmotor_fw_update_fsm_step0x08030FF4(was already named), set its prototype, and created + named a new functionmotor_post_update_cb0x08030994(the 30-byte one-shot scheduler callback — body decode-pending) — program saved. The USART IRQ-handler pass renamed 4 more (usart3_irq_handler0x080362D0,uart5_irq_handler0x08036560,uart4_irq_handler0x08036424,usart6_irq_handler0x0803669C← wereFUN_*) and set 6 prototypes (those 4 plusbus_tx_enqueue_byte0x0803639C/bus_rx_byte_locked0x080363EC, re-typed to returnint) — program saved. No new functions created, sofunction_countstays 815. The USART2/UART8/UART7 byte-pump pass renamed 14 moreFUN_*and created + named one new function (console_printf0x080367F0): the three remaining serial byte-transport triads — USART2/GSM modem (modem_uart_putc0x080360F8,modem_uart_flush0x08036130,modem_uart_rx_byte0x08036144,usart2_irq_handler0x0803617C), UART8/BLE-debug (uart8_tx_byte0x08036A38,uart8_rx_byte0x08036A70,uart8_irq_handler0x08036AA8), and UART7/console (uart7_tx_byte0x08036754,uart7_puts0x0803678C,uart7_write0x0803679E,uart_rx_ringbuf_get_byte0x080367B8,console_printf0x080367F0,uart7_irq_handler0x080368D4) — plusringbuf_free_space0x080318EC, and the two referenced leavessystem_reset0x08035CE8/console_io_table_install0x080430D8. 15 prototypes set, 6 RAM globals labelled (g_uart7_dev_pp0x200097E4,g_uart8_dev_pp0x200098E4,g_gsm_dev_pp0x200099A4,g_console_uart_ctx0x20003C34,g_console_state0x20004D2C,g_console_log_echo0x20000083), program saved. One new function created →function_countwas then 816. The USART1 console-port pass renamed 6 moreFUN_*and created + named one new function (usart1_printf0x08035EBC): the USART1 console byte transport — twin of the UART7 block —usart1_tx_byte0x08035E28,usart1_puts0x08035E5C,usart1_write0x08035E6E,usart1_rx_byte0x08035E88,usart1_printf0x08035EBC,usart1_irq_handler0x08035F98, plus the referenced installerusart1_io_table_install0x0804309C. 7 prototypes set, 2 RAM globals labelled (g_usart1_dev_pp0x200098A4,g_console_port_sel0x20000114), program saved. One new function created →function_countis now 817.
Refreshed census (now re-confirmed by a 14-agent verification + 3-range completeness
critic, every remaining function decompiled): of the 130 FUN_ still in the program,
exactly — 116 vendor/stock-HAL (NOT worth decoding, HAL boundary) + 14 phantom
already-sourced (Ghidra-split tails / externs whose OEM addr is cited in src). The 12
"genuinely-undecoded bespoke" carried below are now ALL SOURCED (this pass). The census
critic examined 137 FUN_ across 0x08020000–0x0805FFFF and found 0 missed bespoke —
it positively re-ID'd the previously-fuzzy strays too (0x080313F8=newlib _sbrk guard;
0x08032BB4/C60/C6A=stock HAL_SPI_MspInit/MspDeInit for SPI1; 0x0803C3B6=
HAL_TIM_PWM_MspInit tail). The decomp is complete for bespoke VanMoof logic — only
newlib/libgcc + CubeF4 HAL remain extern by design.
The 14 phantom already-sourced (do NOT re-count as pending): the 8 modem step-tails
0x080301E6–0x08030642 (split loop-bodies of the sourced modem_step_*),
tx_table_handle_in_use (0x08039FE0, ssp.c), console_activity_timer_rearm (0x08029FE8,
console_edit.c), the HAL_TIM_PWM_MspInit TIM1 tail (0x0803C3B6, tim.c), and the HAL
boundaries HAL_RTC_SetTime/SetDate (0x08022F44/0x08023042, rtc.c) + the I2C2-DMA write
(0x08024BC0, display.c) — all kept extern by design.
The genuinely-undecoded bespoke — ALL 12 NOW SOURCED (this pass, 11/11 adversarially verified PASS):
HIGHDONE →0x08043148console_passthrough_io_install(console.c) (prior pass).0x080317F4→comm_register_buffer+0x08035D0C→comm_buffers_register_all(newcomm.c) — the 16-slot inter-module comm-buffer registry (pool @SRAM0x2000069C: init-flag byte + 16×0xC records {buf, size(u16), 3×u16 zeroed});comm_buffers_register_allwires all 16 link buffers from the five per-port contexts (0x2000094C/0x20004DB4/0x20001A44/0x20002B3C/0x20003C34).0x0803805C→rtc_msp_init+0x08038A04→rtc_wakeup_event_cb(rtc.c) —HAL_RTC_MspInit(enablesRCC_BDCR.RTCENvia the bit-band alias0x42470E3C+ NVIC IRQ 3 = RTC_WKUP) and the wake-up event latch (sets*(*(u32**)(0x20000094+0x24))=1).0x08040288→HAL_CRC_MspInit+0x080402B8→HAL_CRC_MspDeInit+0x080402E8→crc_accumulate_device_uid(crc.c) — CRC clock-gate on/off (RCC_AHB1ENRbit 12, CRCEN, base0x40023830) and a CRC-32 over the 96-bit device unique-ID (0x1FFF7A10) used byconsole_cmd_show.0x0803A538→ble_interval_debounce(ble.c) — the BLE telemetry-change 8000-tick one-shot debounce (scheduler_*, timer name"*tmr"), used byble_telemetry_change_broadcast.0x08032CBC→adc_config_shadow_copy(sensor.c) — latches the live ADC sampling config (ADC_CTX+0x18/0x1c/0x20) into the shadow (+0x24/0x28/0x2c) while status byte+0x22is clear.0x0803D648→sms_tracking_latch_once+0x0803D65C→sms_tracking_get(modem.c) — the SMS info-tracking one-shot latch + getter (flag @0x2000839C).0x08043EB8=Default_Handler(already reconstructed instartup_stm32f413.S— the spin target of the unused vector slots; renamed in Ghidra this pass, not a new function) and0x08043EC8→reset_region_empty_stub(an unreferenced no-op frame in the reset region; reproduced in startup.S).
(Historical 4-range survey below, retained for the vendor/HAL detail.)
Not worth decoding (~130, vendor/stock — decode only for a byte-exact build):
- newlib / libgcc (~30): the
0x08020400–0x0802290Cblock —vfprintf/_printf_i/pad,_malloc_r/_free_r/_realloc_r/_sbrk_r,__aeabi_dmul/dadd/d2iz/uldivmod,
Not worth decoding (~130, vendor/stock — decode only for a byte-exact build):
- newlib / libgcc (~30): the
0x08020400–0x0802290Cblock —vfprintf/_printf_i/pad,_malloc_r/_free_r/_realloc_r/_sbrk_r,__aeabi_dmul/dadd/d2iz/uldivmod,memchr/memmove/memmem/strnlen. - Stock CubeF4 HAL (~85): CAN bxCAN (9), RTC Init/SetTime/SetDate (6), FLASH
option-byte/RDP + program byte/half/word + unlock/lock/erase (16), GPIO AFR/MODER/EXTI (6),
UART SetConfig + full IT engine (14), I2C master/mem/DMA IRQ interior (16), TIM
Start/Stop + IRQ interior stubs (6), RCC/MCO/clock-freq (3), CRC/SysTick (3), and ~15
empty
HAL_*_Callbackweak stubs.
Worth decoding — bespoke app (~45), by cluster (priority order):
Modem AT-command state machine (HIGH, ~11):DONE / was over-counted. The eight "AT-sequence runners"0x080301E6/08030286/08030326/080303C6/08030466/08030502/080305A2/08030642are Ghidra-split tails of the already-sourcedmodem_step_sms_init/_sms_read/_sms_write/_ctx_activate/_ctx_deactivate/_ping_send/_message_send/_location_sendinmodem.c(each parent has a conditional branch Ghidra promoted to a separate function — they are NOT undecoded).0x0802F3A0was the already-sourcedmodem_at_response_match(renamed in Ghidra).0x08035C94(the RX response-line accumulator, misnamedmodem_uart_tx_byte) was the only genuinely-unsourced piece → now sourced inmodem.c.0x080330E4is a status→string mapper belonging to cluster #4 (announce dispatcher), not modem. Net: modem layer complete; the 8 tail-fragments remain as cosmeticFUN_artifacts of sourced parents.- Debug-console command engine + VT100 editor (HIGH, ~13): 11/13 DONE → new
console_edit.c(10/10 adversarially verified):console_cmd_match(0x08040904),console_history_init/_rotate(0804094C/080409A0), the two dispatchersconsole_dispatch_login(08040A00, pre-auth: runs thelogincmd) /console_dispatch_command(080426BC, logged-in match+arg-split), history navconsole_history_recall/_prev/_next(08041184/080411CA/080411FA), tab-autocompleteconsole_autocomplete_apply/_search(08041232/0804125C), and the VT100/CSI→keycode decoderconsole_vt100_decode_key(080431A4), and now the line editor itselfconsole_line_editor(080434F8, ~200 lines — insert/overwrite, backspace/delete with mid-line redraw, Home/End/cursor moves, history up/down, tab-completion echo, Enter→dispatch; 3 adversarial-verify catches folded in: the Delete reposition loop's+1, the Tabwrite(ac_match,ac_len)source, and the pre-login "recognised" British-spelling string vs the logged-in "recognized"). One non-editor stray deferred:FUN_08043148— not a prompt-setup but a 3rd console-I/O-table installer (writes g_log_func[0..4] by port selector) whose installed [0]/[2]/[3] point at undefined functions0x08036B74/76/78; resolve those first. SSPM inter-module bus (HIGH)DONE →ssp.c—sspm_bus_recv_frame(SLIP+CRC16 RX de-framer),tx_table_release_by_handle/tx_table_free_count(08039F90/0803A510;08039FE0was alreadytx_table_handle_in_use),sspm_ble_cmd_bridge/sspm_ble_read_bridge.Announce/telemetry dispatcher (HIGH)DONE →battery.c—manchester_announce_decode(serial/fw/SoC/SoH·NoC·WST),wst_status_to_string,staged_msg_crc16,tim10_announce_period_cb(TIM10 double-buffer). Verify caught the SoC%dbeing signed (OEMldrsb) — fixed.HDC1080 driver (MED)DONE →sensor.c—hdc1080_set_pointer+hdc1080_read(soft-float, 0.1 °C / %RH; constants 2⁻¹⁶/165/40/10/100, signed temp / unsigned RH).EXTI/TIM filters (MED)DONE —sensor_deglitch_filter(6-sample min/max-reject mean,0x08038ED4) →sensor.c;console_magic_sequence_match("\x1b[14~"gesture ring,0x08037188) →console.c. (The named app-hooksexti9_5_app_hook/tim7_app_hookthat call the filter, plus0x08029FE8/0x0803A538timer glue, remain bodies-deferred.)I2C event callbacks + bus scan (MED)DONE →i2c.c—i2c_tx_complete_callback,i2c_error_callback(+display_request_recovery),i2c_bus_scan(diagnostic 0..0xFE probe).- Low/glue: misc setters/getters/flag-latches (
0x0803D648/0803D65C,08040288/080402B8bit-toggles,08032CBCshadow-reg copy, MspInit/DeInit strays08032BB4/08032C60/08032C6A/0803805C/0803C3B6— the last is theHAL_TIM_PWM_MspInitfall-through fragment).
-
Version diff 1.07.06 → 1.08.02 documented (batteryware-style). Imported the next OEM release (
mainware_1.08.02.bin, +1360 B, May 2022) fresh into Ghidra/diff/@0x08020000 and ran a function-level diff (position-independent body-hash + fuzzy match + per-functiondiff_functions; survey + 4 deep-dives, adversarially verified). 539/836 named bodies byte-identical; 167 changed (158 genuine). Three user-visible features: off-road region REMOVED from the BLE control surface (off-road sentinel 0xFF now → EU, restriction message/branch deleted), thegearconsole cmd repurposed from a SOC-override into a real shifter driver (0=off, 99=reset SM), and the wake-reason taxonomy expanded 9→13 + a new wake counter. Docs:changelog-1.07.06-to-1.08.02.md+compare-1.07.06/(README + wake.md/gear.md/region.md).src/reconstructs 1.07.06 (the "before" side). -
flash_read_config_with_crc_restoreSOURCED + SMS remote-control documented + 9 more validated — all PASS. The 4th deferred named-extern is now reconstructed (PASS-verified, build clean text 1996):flash_read_config_with_crc_restore→ flash.c (0x08031784, the config-load twin ofconfig_persist_dual_bank: copy bank A0x08008000→ CRC-verify 0x34 words → on fail log" ERR:Read flash copy"+ read bank B0x0800C000→ if B valid re-commit BOTH banks to heal A; returns 0 if either valid / 1 if both corrupt; the 0xC0-byte payload atout+0x10passed by value, matching the OEM stack copy).sim_iccid_checkre-validated PASS — the prior inner-deref fix (*(char**)(ctx+0x3E8)+0x50) is confirmed correct against the OEMldr [ctx,#0x3E8]; +0x50.modem_sms_dispatch_commanddocumented indocs/modem.md(the inbound-SMS remote-control surface): the#<8-char code>*<cmd>wire format, all 5 commands (key/TrackingOn/TrackingOff/ping/makeNoise), thepingJSON status payload + acurlexample, and the 8-char-code security analysis — it's a backend-supplied correlation token, NOT an auth secret: the firmware never stores or validates it (nor the sender number); the only anti-theft auth is the SIM lock (sim_iccid_check) + the bike's private provisioned number. Two doc/comment corrections folded in: the TrackingOn gate is GPIOC PC2 (not GPIOB), and the ICCID is*(ctx+0x3E8)+0x50(inner deref). 8 more validated PASS (modem SMS-read/location-send/ctx-activate steps, console distance/shipping, battery_on_detect_step, shifter_seq_status_poll_step, ssp_ble_dispatch) — 0 bugs. flash.c decomp-c 8→9 (total 518→519); validation running-total ~96 functions across 8 batches, 11 real bugs fixed. -
3 deferred named-externs SOURCED (the validation sweep's leftovers) + 6 more validated — all PASS. The three functions earlier flagged as named-only (stack-aliasing/intricate) are now reconstructed and adversarially verified 3/3 PASS (build clean text 1996, 0 warnings):
modem_sms_dispatch_command→ modem.c (0x0803D668, the inbound-SMS remote-command interpreter —#<8-char code>*<cmd>→key/TrackingOn/TrackingOff/ping/makeNoise; the SMS half of the anti-theft surface parallelingble_cmd_dispatch. All command + log + format strings resolved byte-for-byte from rodata; thepingJSON status report does signed/10on battery (int16 ctx+0x3D2) and distance (int32 ctx+0x31C), reads the firmware version from flash0x08020004, and formats the MAC fromctx+0x390..0x395).flash_config_bank_write→ flash.c (0x080316D0, the single-bank config writer behindconfig_persist_dual_bank: erase → CRC the first 0x33 words into the record's reserved last word → program 0xD0 bytes → self-checking CRC verify from flash; modeled with aunion{w[0x34]; {hdr[4];boot_cfg_block;}}for the by-value record + indexed CRC store).save_state_record_to_eeprom→ app.c (0x0803E2CC, the dual-copy EEPROM state writer: CRC first 0xE words into word 0xE → write at EEPROM offsets 0 and 0x40 with a 5 ms/watchdog gap; same union pattern, link-compatible with all ~19 existing callers which each keep their own extern). 6 more validated PASS:eeprom_write_region,console_soc_set,modem_step_message_send,modem_at_response_copy,matrix_draw_icon,volume_low_set. New deferred-extern found (NOT a bug):flash_read_config_with_crc_restore(0x08031784, the config-load twin ofconfig_persist_dual_bank— reads bank A, CRC-verifies, falls back to bank B and heals bank A on recovery) is a named-only extern; spec captured, a sourcing candidate. A further 10-function batch then ran (9 PASS) and caught 1 more real bug —modem_step_powerondrove the wrong GPIO for the modem main-supply enable:MODEM_PWR_EN_PINwas0x0010(PB4) but the OEM writes GPIOB mask 0x4 = PB2 (confirmed by my own disasm:r5=GPIOB base, firstmovs r1,#0x4). Fixed the macro (also used by poweroff → both now correct) and the "PB4"→"PB2" doc references; also restored timer[0]'s OEM name"interval_tmr"(was"timeout_tmr"). The other 9 PASSed (console_cmd_battery/_adc,modem_handle_uuloc,bms_telemetry_unpack,battery_charge_display_step,slip_send_frame,matrix_draw_level_bar,shifter_send_gear,led_driver_standby_write). 3 funcs sourced (decomp-c 515→518); modem prototype set in Ghidra + program saved. -
Validation sweep, batches 2-5 (lean re-verification workflows) — 9 more real bugs found + fixed. After the over-scoped 48-agent run bottlenecked on the single Ghidra MCP server, the remaining important functions were re-validated in lean ~11-agent batches. Batch A (10 funcs + shifter.c hazard): all PASS, 0 missing-arg bugs in shifter.c (the bare-paren
shifter_send_gear()/shifter_crc_update()calls are decompiler artifacts — the disassembly confirms r0 holds the value at thebl). Batch B (11 funcs + states.c hazard): 6 PASS, 5 FIXED, 1 deferred-extern noted: (1)sim_iccid_check(modem.c) — address-vs-deref: the ICCID is reached via a pointer stored atctx+0x3E8(OEMldr [ctx,#0x3E8]then+0x50), not a flatctx+0x3E8+0x50offset — the anti-theft ICCID compare + log were reading the wrong address. Now*(char **)(ctx+0x3E8)+0x50. (2)modem_at_exec(modem.c) — wrong arg: the>-prompt echo passed'\0'instead of the'>'rx byte;'\0'fails the accumulator's printable filter so the>was dropped, breaking everyexpect==">"data-prompt wait (CMGS SMS send). Now passesrx. (3)HAL_I2C_Master_Transmit(i2c.c) — missing__HAL_I2C_CLEAR_ADDRFLAG: the SR1-then-SR2 read afterMasterRequestWritewas omitted; without it SCL stays stretched and TX never advances. Added. (4)HAL_I2C_Mem_Read(i2c.c) — same ADDR-clear missing in all 4XferSizebranches (and the>=3else branch was absent entirely); reads would time out. Rewrote the selector to the canonical CubeF4 ordering (==0 clear,STOP / ==1 ACK-off,clear,STOP / ==2 ACK-off,POS,clear / else clear). (5)led_driver_set_shipping_mode()(states.c:1744) — missing arg: the K&R extern hid a dropped argument (the OEM passes the post-decrementedG_STATE[0x27]). Nowled_driver_set_shipping_mode(G_STATE[0x27]). Batch C (11 funcs + display.c hazard): 9 PASS, 2 FIXED, display.c hazard clean: (6)HAL_I2C_Master_Receive(i2c.c) — the SAME ADDR-clear omission in all 4XferSizebranches as its Transmit/Mem_Read siblings (the third I2C HAL function with this bug) → receives would hang. Added the per-branch SR1/SR2 clear (==0 clear,STOP / ==1 ACK-off,clear,STOP / ==2 ACK-off,POS,clear / else ACK-on,clear). (7)ssp_rx_byte(ssp.c) — extra dereference: masked RXNEIE at*(*(*(0x20009864))+0xC)(three loads) where the OEM does two (*(0x20009864)is the UART5 base,+0xCis CR1) — so it poked a bogus address and never gated the UART5 RX IRQ around the ring read. Removed the extra hop (now matches theuart_send_byteidiom; verified the other byte-pumps don't share it). The whole CubeF4 I2C transfer driver is now validated (Transmit/Receive/Mem_Read fixed, Mem_Write/Init clean). PASSed clean across C:HAL_I2C_Mem_Write,HAL_I2C_Init,usart2/3_irq_handler,uart5_irq_handler,sspm_bus_get_byte,mainware_boot_init_sequence,stc3115_i2c_read,light_pattern_action_apply; display.c hazard sweep found 0 missing-arg / address-vs-deref issues (the request setters correctly take descriptor addresses). Batch D (10 funcs + ble.c hazard): 6 PASS, 2 FIXED, 2 deferred-externs, ble.c hazard clean: (8)login_handler(console.c) — AUTH-STATE DESYNC (address-vs-deref + struct-model): the OEM (and our ownlogout+console_edit) write/readlogged_in(+0x2D9)/fail_count(+0x2E0) at theg_app_statebase (0x20009368), butlogin_handlerwrote them throughctx_sub(→0x200083A8) — so a correct password set the wrong byte and the session never registered as logged-in (login effectively broken). Root cause:logged_in/fail_countwere mis-declared insidestruct session_ctxwhen they actually live in the outerstruct app_stateadjacent toctx_sub. Fixed the struct model (moved both fields toapp_statewith new_Static_asserts at 0x2D9/0x2E0) andlogin_handler(nowg_app_state.logged_in/.fail_count), matching the other two accessors. Also restored the OEM"Please wait..\r\n"lockout string (was missing\r\n). (9)flash_erase(flash.c) — wrong log string: logged"Flash erase error %d"; OEM string @0x08052F18is"Sector error %d\r\n". Corrected (observable-output divergence). PASSed clean:HAL_UART_Init,flash_write,config_persist_dual_bank,console_region_set,matrix_draw_number,modem_at_response_match; ble.c hazard sweep found 0 missing-arg / address-vs-deref issues (config_persist by-value struct + request- setter descriptor addresses all correct). Deferred-externs noted (named-only, no committed body — not bugs):flash_config_bank_write(0x080316D0) andsave_state_record_to_eeprom(0x0803E2CC) joinmodem_sms_dispatch_commandas documented stack-aliasing-ABI named-externs; the verifiers captured each one's full OEM spec for a future reconstruction pass. Deferred-extern noted (not a bug):modem_sms_dispatch_command(0x0803D668, the inbound-SMS remote- command interpreter — the SMS half of the anti-theft surface) is a named extern with no committed body; the verifier captured its full OEM spec (8-char code,#/*delimiters,key/TrackingOn/TrackingOff/ping/makeNoisecommands + their persist/BLE actions) for a future reconstruction. PASSed clean this pass:battery_charge_lookup,ble_read_request_dispatch,pack_validate,boot_init_cold,light_sensor_read_step,lis3dh_accel_init, the 10 of batch A. Build clean text 1996, 0 warnings. -
Validation sweep of the already-sourced C (29-agent re-verification workflow) — 1 real bug fixed. A curated high-risk set of 28 already-committed functions (the UART4↔5/UART7↔8 swap-edited serial layer, the fixed-point/soft-float math, the SLIP/Modbus framing, and structural spot-checks of the six giant hand-assembled engines) was re-derived from raw OEM disassembly and diffed against the committed source; a 29th agent re-checked the whole serial-peripheral mapping chain (init Instance base → ISR home file → NVIC slot →
hardware.md) for coherence. Result: 25 PASS + 3 resolved. (1) REAL BUG FIXED —bat_modbus_master_step(battery.c case 2):bus_crc16_update()was called with no argument (the K&Rextern int bus_crc16_update()let it compile), so the Modbus function-code byte could be dropped from the running CRC and every BMS response would fail the case-7 trailing-CRC check — nowbus_crc16_update(rx), matching all 24 sibling calls; swept battery.c to confirm it was the only missing-arg call. (2)+(3) behaviour-equivalent, documented (not changed):sspm_bus_recv_frame's"PE\r\n"log goes throughg_log_func(printf slot) where the OEM uses theputsvtable slot — byte- identical output for a constant string, and consistent with how all of ssp.c models logging;console_printfkicks the watchdog unconditionally where the OEM throttles ~1-in-256 (IWDG never times out either way) and returnslenvs the OEM's incidental-r0 (printf return is ignored) — both within the behaviour-equivalent goal, now noted in code comments. The serial-mapping coherence agent + theuart4_irq_handlerverifier both PASS, independently re-confirming the UART4/5/7/8 correction. Build clean text 1996, 0 warnings. -
Remaining bespoke glue SOURCED (12 funcs across new
comm.c+crc.c/rtc.c/modem.c/ble.c/sensor.c/startup_stm32f413.S) — the bespoke application layer is now complete. All build clean text 1996, 0 warnings, and adversarially verified by a 14-agent workflow: 11/11 new decodes PASS (high confidence, no issues) + a 3-range completeness critic that examined 137FUN_and found 0 missed bespoke (it could not falsify the completeness claim). Newcomm.c/comm.h:comm_register_buffer(0x080317F4, the 16-slot inter-module comm-buffer registry — pool @SRAM0x2000069C, one init-flag byte then 16×0xC records{buf, size(u16), 3×u16 zeroed}; first call zero-inits the occupancy field, alloc returns the first free slot) andcomm_buffers_register_all(0x08035D0C, 16 registrations wiring each per-port context's RX/TX rings — bases0x2000094C/0x20004DB4/0x20001A44/0x20002B3C/0x20003C34; was an extern stub inmain.c, now sourced).crc.c:HAL_CRC_MspInit/HAL_CRC_MspDeInit(0x08040288/0x080402B8, gateRCC_AHB1ENRbit 12 CRCEN on/off, guarded onhcrc->Instance == CRC 0x40023000; MspInit keeps the CubeF4 read-back delay) andcrc_accumulate_device_uid(0x080402E8, copies the 96-bit device UID0x1FFF7A10to a stack buffer →HAL_CRC_Accumulate(handle 0x20009D90, buf, 3), returns the CRC via r0-passthrough; theconsole_cmd_showdevice hash).rtc.c:rtc_msp_init(0x0803805C, theHAL_RTC_MspInitoverride called fromHAL_RTC_Init— setsRCC_BDCR.RTCENvia the peripheral bit-band alias0x42470E3Cand enables NVIC IRQ 3 = RTC_WKUP, guarded onhrtc->Instance == RTC 0x40002800) andrtc_wakeup_event_cb(0x08038A04, the wake-up event latch fired byrtc_wakeup_irq_handler:*(*(u32**)(0x20000094+0x24)) = 1).modem.c:sms_tracking_latch_once/sms_tracking_get(0x0803D648/0x0803D65C, the SMS info-tracking one-shot latch + getter on flag @0x2000839C).ble.c:ble_interval_debounce(0x0803A538, used byble_telemetry_change_broadcast— while any watched change bit is set it keeps an 8000-tickscheduler_*one-shot armed in*slot(timer name"*tmr"@0x080529D0) and reportsscheduler_slot_is_idle; releases + reports ready when the bits clear; preserves the OEM 6-arg stack ABI).sensor.c:adc_config_shadow_copy(0x08032CBC, latches the live ADC sampling configADC_CTX+0x18/0x1c/0x20into the shadow+0x24/0x28/0x2cwhile the status byte+0x22is clear; OEM loads+0x20as a word, stores its low half).startup.S:0x08043EB8was identified as the OEMDefault_Handler(the spin target of the unused vector slots — already reconstructed in startup.S; renamed + marked no-return in Ghidra this pass, NOT a new function), andreset_region_empty_stub(0x08043EC8, an unreferenced no-op frame just past Default_Handler — exact instruction sequence reproduced for reset-region coverage). 12 Ghidra renames- 12 prototypes + 1 no-return, program saved. pending 142→130, decomp-c 504→515; the JSON needs a
DumpMainwareProgram.javare-dump.
- 12 prototypes + 1 no-return, program saved. pending 142→130, decomp-c 504→515; the JSON needs a
-
Console passthrough I/O-table installer →
console.c(1 func + 3 no-op stubs, PASS-verified, build clean text 1996) —console_passthrough_io_install(0x08043148, the survey's lone HIGH-value remaining item). On entering thegsmdebug/bledebugraw-UART passthrough,light_tick_updatecalls it to install a silentg_log_functable (0x20009D98): slots [0]/[2]/[3] printf/puts/write become the no-op stubs0x08036B78/74/76(allbx lr-class — created+namedio_noop_printf/puts/write) so firmware log output can't corrupt the forwarded byte stream, while [1]/[4] tx/rx bind to the active console port (UART7, or USART1 wheng_console_port_sel0x20000114==1). The two branches differ only in slots [1]/[4]; verified byte-for-byte against the literal pool @0x08043180. (Also added the priorconsole_magic_sequence_matchprototype to console.h.) 1 rename + 3 functions created + 1 prototype, program saved. pending 143→142, decomp-c 503→504. -
Triage clusters #3–#7 SOURCED (16 funcs across
ssp.c/battery.c/sensor.c/console.c/i2c.c) — the four HIGH + three MED clusters from the remaining-FUN_ triage, all build clean text 1996, 0 warnings, and adversarially verified by a 15-agent fan-out workflow (14 PASS + 1 confirmed FAIL, fixed → 16/16 faithful). #3 SSPM RX →ssp.c:sspm_bus_recv_frame(0x0803A0C0, the SLIP+ CRC-16 RX de-framer — twin ofsspm_bus_send_frame; SLIP state @0x20007F94, the returned status vs persistent state byte diverge per-branch, reproduced exactly),tx_table_release_by_handle/tx_table_free_countover the 16×0x18 endpoint table @0x20007E14, and thesspm_ble_cmd_bridge/sspm_ble_read_bridgetail-call bridges to the BLE dispatchers. #4 announce →battery.c:manchester_announce_decode(0x08043B28, decodes the staged inter-module announce frame types 0 serial / 1 fw-version / 2 SoC / 3 SoH·NoC·WST into the telemetry cache + logs),wst_status_to_string(WST_NONE/DISCHARGE/CHARGE/BYPASS/UNKNOWN),staged_msg_crc16(Modbus CRC-16, implicit-r0 return), andtim10_announce_period_cb(0x08043DE0, TIM10 period-elapsed cb reloading CNT to Period−0x682 and committing the pending→active double-buffer @0x200096D4). Verify-caught bug (fixed): thesoc %dlog loaded the cached SoC withldrsb(signed) — peer-controllablemsg[1]≥0x80 must print negative, so the arg is now(int8_t)cache[0]. #5 HDC1080 →sensor.c:hdc1080_set_pointer+hdc1080_read(I²C dev 0x80, 4-byte read → soft-float convert, temperature to 0.1 °C signed via__aeabi_d2iz, RH to % unsigned via__aeabi_d2uiz; constants 2⁻¹⁶/165/40/10/100, the −40 done as__aeabi_dsub). #6 →sensor.c/console.c:sensor_deglitch_filter(0x08038ED4, 6-sample ring, drop min+max,(sum−max−min)>>2) andconsole_magic_sequence_match(0x08037188, 5-byte rolling ring @0x20005E20matched against the literal"\x1b[14~"F4 sequence, alight_tick_updatehelper). #7 I²C →i2c.c:i2c_tx_complete_callback/i2c_error_callback(per-controller log; an I2C1 error also callsdisplay_request_recovery) andi2c_bus_scan(diagnostics_run_step's 0..0xFE probe of both buses viaHAL_I2C_IsDeviceReady). 16 renames + 13 prototypes, program saved. pending 167→151, decomp-c 487→503; function_count stays 817. -
ES3 console command engine + VT100 key decoder →
console_edit.c/console_edit.h(NEW) — the interactive front-end that drives the 49-command console (the per-command handlers were already inconsole.c). 11 funcs, 10/10 adversarially verified, build clean text 1996, 0 warnings.console_cmd_match(0x08040904, command/whitespace token match); the 9-slot input-history ringconsole_history_init/_rotate/_recall/_prev/_next(0804094C/080409A0/08041184/080411CA/080411FA) over nine 0x51-byte line buffers at0x20009368+i*0x51, ring-ctrl block at ctx+0x2E4; tab-autocompleteconsole_autocomplete_search/_apply(0804125C/08041232, prefix-scan the cmd table viabounded_strncmp); and the two command dispatchers over the 49-entry table @flash0x0804F5C4:console_dispatch_login(08040A00) is the pre-auth path — every typed line before login is matched to the literal"login"command and run through its handler (the gate);console_dispatch_command(080426BC) is the logged-in path (match line → split args at first space → handler). The VT100/ANSI escape decoderconsole_vt100_decode_key(080431A4) reads keys via theg_log_funcI/O vtable slot [4] and folds CSI (ESC [) / SS3 (ESC O) sequences into internal key codes 0x80–0x98 (arrows/F-keys/Home/End/Ins/Del/PgUp/PgDn), with a 10-tick scheduler timer to emit a loneESC. Escape state at ctx+0x368/+0x369. Follow-on (same module): the line editorconsole_line_editor(0x080434F8) — the ~200-line glue loop pulling one decoded key and applying it (insert/overwrite, BS/DEL mid-line redraw via\x1b[K/\x1b[1D, Home/End/left/right, history up/down, tab-completion echo of the staged match, Enter→console_dispatch_login/_command, Esc→ clear). Active line at session+0x350, in-line cursor+0x36A, logged-in flag+0x2D9(pre-login echoes'*'). Adversarial verify caught 3 real bugs (now fixed): Delete's reposition loop emits one fewer\x1b[1Dthan Backspace (+1start), Tab echoesac_match/ac_len(not the line), and the pre-login unknown-command string is the British "recognised" while the logged-in one is "recognized". Still out (not the editor):FUN_08043148, a 3rd I/O-table installer pointing at undefined0x08036B74/76/78. 12 renames + 8 prototypes, program saved. pending 179→167, decomp-c 475→487; function_count stays 817. -
EEPROM read surface →
eeprom.c(2 funcs, build clean text 1996) — the on-board AT24C EEPROM (device 0xA0, I2C3) read side, joining the existing region writer.eeprom_read_id_block(0x0803E138) writes the command byte 0xFA then reads 6 bytes, returning(recv | xmit) & 0xFF(0 = OK); probed once at boot. A user cross-reference from VanMoof firmware 1.9.x identifies the identical routine there asSecurity_GetLockState— the 6 bytes are the bike's lock/security state (recorded in a Ghidra plate comment + the source comment; name derived from this binary's own disassembly per the separate-target discipline, the sibling name as confirmation).eeprom_read_bounded(0x0803E174, ex-FUN_) = the boundedHAL_I2C_Mem_Readprimitive (rejects len 0 or a read past the 0x80-byte device) behindeeprom_read_config_with_crc_fallback. The no-bound-check siblingeeprom_read_raw(0x0803E182) was named (not sourced). 2 renames + 2 prototypes + 1 plate comment, program saved. pending 183→181, decomp-c 472→474, named unchanged 135; function_count stays 817. -
STC3115 fuel-gauge driver →
stc3115.c/stc3115.h(NEW) — the ST STC3115 LiPo coulomb-counter/OCV gas gauge on I2C3 (handle0x20009B04, shared with the LIS3DH), 8-bit address 0xE0. Sourced the transport + RAM-CRC + conversion + measurement layers (16 funcs, adversarially verified 15/15 PASS, build clean text 1996): I²C primitivesstc3115_i2c_read/_write(0x080392C0/0x08039448, reg-addr write then read/write N), the register accessorsstc3115_read_reg/write_reg/read_word/write_word/read_block/write_block(0x080393DC/0x080394A6/0x080393FE/0x080394BE/0x080392F4/0x0803948C— read_reg/write_reg were extern stubs insensor.c, now bodied), the 16-byte SRAM RAM-mirror layerstc3115_ram_read/ram_write/ram_init/ram_crc8/ram_update_crc(0x08039302/0x0803949A/0x08039294/0x0803924C/0x08039280; g_stc3115_ram @0x20006E80, signature 0x53A9 at [0], CRC-8 poly 0x07 at [15]), the fixed-pointstc3115_conv(0x0803923C,((v*scale)>>11 +1)/2),stc3115_check_id(0x08039428, reg 0x18==0x14 part-ID gate), and the measurement burst-readstc3115_read_measurements(0x0803930E): reads regs 0..14 and converts SOC%/voltage(12-bit, scale 0x2333)/current(14-bit, scale 0x968)/temp(°C×10)/counter/OCV into the caller's struct (out[1..7]). The verifier confirmed the byte→register map and the current↔out[4]/voltage↔out[3] index split exactly. Register map documented instc3115.h(0 MODE, 1 CTRL, 2-3 SOC, 4-5 COUNTER, 6-7 CURRENT, 8-9 VOLTAGE, 10 TEMP, 13-14 OCV, 0x13 CC_CNF, 0x14 VM_CNF, 0x18 ID, 0x20.. RAM). The config/startup/SOC-algorithm layer (stc3115_apply_config,startup_from_ocv/restore,init_device, the periodicstc_read) was named + prototyped (extern, deferred —apply_confighas decompiler-dropped varargs in itswrite_wordcalls that need disasm-level resolution;stc_readis the large SOC-tracking algorithm with 64-bit reciprocal-multiply divides).sensor.c's existing externs are satisfied by the new module. 18 renames + 11 prototypes + 1 RAM label, program saved. pending 201→183, decomp-c 456→472, named 133→135; function_count stays 817. -
Timer subsystem →
tim.c/tim.h(NEW) — the board's TIM bring-up, sourced (9 funcs, adversarially verified 7/7 PASS, build clean text 1996). Board init wrappers (were named-extern, now bodied):tim1_pwm_init(0x0803C4F4) — TIM1 three-channel PWM for the lamp LEDs (Prescaler 0x960, Period 99; CH1/2/3 → HAL_TIM_PWM_ConfigChannel OCMode PWM1 0x60; ConfigBreakDeadTime),tim6_init(0x0803C2E0, psc 0x42/period 0x32),tim7_init(0x0803C32C, psc 0x4AF/period 10000),tim10_init(0x0803C37C, psc 0x5F/period 5000, base only — no clock-source cfg),tim_channel_enable_output(0x08027988= HAL_TIM_PWM_Start core: CCxChannelCmd + advanced-timer MOE + CEN unless trigger-mode 6). MSP callbacks reconstructed (the GPIO-AF + RCC-clock + NVIC glue, were the lastFUN_in the0x0803C2xx–C5xxblock):HAL_TIM_PWM_MspInit(0x0803C3AC, TIM1: APB2ENR.TIM1EN + NVIC IRQ25),HAL_TIM_Base_MspInit(0x0803C3EC, TIM6→APB1.bit4/IRQ54, TIM7→APB1.bit5/IRQ55, TIM10→APB2.bit17/IRQ25 — matches thevectors.cIRQ map),HAL_TIM_MspPostInit(0x0803C494, TIM1 PWM pins PE9/PE11/PE13 → AF1),HAL_TIM_PWM_MspDeInit(0x0803C5D0). Cross-check: the four handles (HTIM10x20009A84, HTIM60x20009A44, HTIM70x20009AC4, HTIM100x20009A04) are byte-identical to theHTIM*handle addresses the vector trampolines pass toHAL_TIM_IRQHandler, and HTIM1 = the lamp PWM object base fromlighting.c. The generic CubeF4 HAL TIM core (Base_Init/PWM_Init/SetConfig/ConfigClockSource/ConfigChannel/4×OCx_SetConfig/ ConfigBreakDeadTime/CCxChannelCmd/PWM_DeInit) was named + prototyped (extern, vendor-class, bodies deferred), as were the non-TIM straysHAL_MspInit(global PWR+SYSCFG) andi2c2_wait_bus_idle. 18 Ghidra renames + 9 prototypes, program saved. pending 219→201, decomp-c 447→456, named 124→133; function_count stays 817. -
NVIC interrupt vector layer →
vectors.c(NEW) + UART4↔UART5 / UART7↔UART8 label correction — decoded the 25 peripheral-IRQ trampolines (0x0803CA20–0x0803CB64) that occupy the external-IRQ slots of the table at0x08020200: thin wrappers the CPU enters on each NVIC line, forwarding to a HAL servicer, a serial byte-pump ISR, or an EXTI-line demux (sometimes after an application pre-hook). Named with CMSIS vector names (RTC_WKUP_IRQHandler,EXTI0/1/2/3/4/9_5/15_10,DMA1_Stream1/DMA2_Stream0,TIM1_UP_TIM10/TIM6_DAC/TIM7,I2C1_EV/ER,I2C3_EV/ER,USART1/2/3/6,UART4/5/7/8). Decoding the table required establishing slot index == IRQ number (verified by direct slot reads: USART1=37, UART4=52, USART6=71, UART7=82, UART8=83), which turned the vector table into an authoritative peripheral-identity oracle and exposed a two-pair naming swap in prior work: the functions labelleduart4_*/uart5_*anduart7_*/uart8_*were each paired to the wrong physical peripheral. Proof (3 independent, 17/17 adversarially verified): (1) init Instance MMIO bases —uart4_initstored0x40005000=UART5,uart5_initstored0x40004C00=UART4;uart7_initstored0x40007800= UART7 was the body labelleduart8, etc.; (2) vector slots — IRQ82(UART7) → the console handler, IRQ83(UART8) → the BLE-debug handler; (3) the console port selector value is literally 7 (g_console_port_sel=7, reset magic0x55AA5507), i.e. UART7. Corrected across Ghidra (26 swap-renames via temp tokens + 25 trampoline + 9 leaf names, program saved) and the whole source/doc tree via an atomic digit-swap (4↔5, 7↔8 onuart/UARTtokens) overbus.c/console.c/main.c/ssp.c/uart.c+ their headers +boot.md/hardware.md/progress.md/README.md(excludingconsole.md, which already correctly saidbledebug→UART8). Physical result: UART7 = ES3 console primary (console.c), UART8 = BLE-debug link (uart.c), UART4 = BMS/battery Modbus 9600 (bus.c), UART5 = BLE data link 115200 (uart.c). The 9 shared leaves the trampolines call (rtc_wakeup_irq_handler,HAL_DMA/TIM/I2C_EV/I2C_ER_IRQHandler,exti4/exti9_5/tim6/tim7_app_hook) are named extern (bodies decode-pending). The trampolines are NOT yet wired intostartup.S(would pull undefined leaf symbols) — they compile and gc away likemain's super-loop until the handler closure is ready. Build clean text 1996, 0 warnings; 47 Ghidra renames, program saved. pending 253→219, decomp-c 422→447, named 115→124. -
USART1 console port (2nd ES3 console, twin of UART7) →
console.c— the last serial byte-pump peripheral, completing the USART byte-pump layer (every USART/UART now has a decoded ISR). USART1 carries the same console as UART7: handle-ppg_usart1_dev_pp0x200098A4(set byusart1_init), rings in the shared ctx0x2000094Cat TX+0x04/ RX+0x08. Six functions sourced (usart1_tx_byte0x08035E28,usart1_puts0x08035E5C,usart1_write0x08035E6E,usart1_rx_byte0x08035E88, the newly-createdusart1_printf0x08035EBC,usart1_irq_handler0x08035F98) — byte-for-byte the UART7 pattern (locked TX/RX primitives + the decompiler-elided RM0430 SR+DR error-clear block in the ISR, both confirmed in raw disassembly; all 6 adversarially verified PASS, implicit-r0 ABI preserved). Dual-console finding: UART7 and USART1 are two physical ports for the same ES3 console — they share the command-mode flag (g_console_state+0x82) and the log-echo flag (g_console_log_echo0x20000083) but track their own consecutive-ESC/-TAB counters (USART1 at+0x80/+0x81, UART7 at+0x83/+0x84). The active port is whichever one's I/O table is bound intog_log_func:usart1_io_table_install0x0804309Cselects USART1 (port selectorg_console_port_sel0x20000114= 1),console_io_table_install0x080430D8selects UART7 (= 7); the escape handlers swap between them. Per-port bootloader hand-off magic differs: USART1 writes0x55AA5501, UART7 writes0x55AA5507(both to SRAM0x20000000).usart1_printfis the USART1g_log_func[0]twin ofconsole_printf(same vsnprintf → optional epoch-prefixed log echo → synchronous drain-buffered TX).usart1_io_table_installnamed (extern, not sourced, likeconsole_io_table_install). Build clean text 1996, 0 warnings; 6 renamed + 1 created + 7 prototypes + 2 RAM labels; program saved. function_count 816→817. -
USART2 / UART8 / UART7 byte-pump triads →
modem.c/uart.c/console.c— sourced the three remaining serial byte-transport peripherals, completing the USART byte-pump layer. Each is the same triad pattern as the prior ISR pass (locked TX/RX primitives that mask the CR1 interrupt-enable bit behind a DSB/ISB, plus a byte-pump ISR carrying the decompiler-elided RM0430 SR+DR error-clear sequence — all 14 functions adversarially verified PASS, error-clear blocks confirmed bit-correct in raw disassembly, implicit-r0 return ABI preserved):- USART2 = the GSM/SARA modem AT channel →
modem.c.modem_uart_putc(0x080360F8, single-byte TX),modem_uart_flush(0x08036130, the puts — already a caller-side extern, now sourced),modem_uart_rx_byte(0x08036144, already an extern, now sourced; returns 1 = got a byte — the old "0 = got" comment was wrong),usart2_irq_handler(0x0803617C). Handle pp0x200099A4(g_gsm_dev_pp), ctx0x2000094C(shared with USART3), TX ring+0x410, RX ring+0x414. Consumed bymodem_at_exec(via the puts) and the command-mode bridge. - UART8 = the BLE-coprocessor debug link →
uart.c(sibling of UART5's data link).uart8_tx_byte(0x08036A38),uart8_rx_byte(0x08036A70),uart8_irq_handler(0x08036AA8). Handle pp0x200098E4(g_uart8_dev_pp), ctx0x20003C34, TX ring+0x970, RX ring+0x974. Reached only via thebledebugconsole command's passthrough bridge. - UART7 = the ES3 debug-console prompt →
console.c.uart7_tx_byte(0x08036754),uart7_puts(0x0803678C),uart7_write(0x0803679E),uart_rx_ringbuf_get_byte(0x080367B8, kept its name, now sourced),uart7_irq_handler(0x080368D4), and the previously-undefinedconsole_printf(0x080367F0, newly created). Handle pp0x200097E4(g_uart7_dev_pp), ctx0x20003C34(shared with UART8), TX ring+0x164, RX ring+0x168.console_io_table_install(0x080430D8) installs these five as theg_log_functable (0x20009D98):[0]=console_printf, [1]=uart7_tx_byte, [2]=uart7_puts, [3]=uart7_write, [4]=uart_rx_ringbuf_get_byte— soconsole_printfisg_log_func[0], the firmware-wide printf every module already calls.console_printfformats withvsnprintfinto a 256-byte stack buffer; wheng_console_log_echo(0x20000083) is set it also mirrors an"%ld "epoch prefix + the message into the SRAM log. It then writes the message out the UART7 TX ring synchronously: TXEIE masked while bytes are queued directly, and on a full ring it enables TXEIE and busy-waits (kicking the watchdog) untilringbuf_free_spacereaches the cap (FIFO fully drained) before resuming; TXEIE left enabled on exit so the ISR sends the tail. (ringbuf_free_space0x080318EC= cap − count, newly sourced toutil.c.)uart7_irq_handlercarries a command-key escape handler: each received byte is watched for ten consecutive ESC (0x1B) → write boot magic0x55AA5507to SRAM0x20000000, log"NVICReset",systick_delay(10),system_reset()(AIRCR, no return); or ten consecutive TAB (0x09) →console_io_table_install(), log"To Commandmode", raise the command-mode flag (g_console_state+0x82, block0x20004D2C). The OEM re-reads DR three times (push + two compares), reproduced faithfully.
- This also identified the mislabelled
light_tick_update(0x080371E8) as the command-mode console↔modem/BLE-debug bridge (routes the console to USART2 viagsmdebugor UART8 viabledebug), not a lamp function (still named, out of scope).
- USART2 = the GSM/SARA modem AT channel →
-
USART RX/TX byte-pump ISRs →
shifter.c/uart.c/ newbus.c/ssp.c(+motor.cfinished) — sourced the four USART interrupt service routines that move bytes between each serial peripheral's register block and its software ring buffers, each colocated with its peer byte primitives:usart3_irq_handler(0x080362D0) →shifter.c— the eShifter (USART3) link; RX ring at ctx+0xC20, TX ring+0xC1C; handle pp0x20009824(g_usart3_handle), ctx0x2000094C(g_usart3_rings). Pairs with the existingshifter_uart_tx_byte/rx_byte.uart5_irq_handler(0x08036560) →uart.c— the BLE-coprocessor (UART5) link; RX ring+0xB40(drained byssp_rx_byte), TX ring+0xB3C(fed byuart_send_byte); handle pp0x20009864, ctx0x20001A44.usart6_irq_handler(0x0803669C) →ssp.c— the inter-module SSPM bus (USART6); RX ring ctx+0xA54, TX ring+0xA50; handle pp0x20009924, ctx0x20002B3C. Pairs withsspm_bus_send_byte/get_byte.uart4_irq_handler(0x08036424) + its two byte primitivesbus_tx_enqueue_byte(0x0803639C) andbus_rx_byte_locked(0x080363EC) → newsrc/bus.c+include/bus.h— the UART4 BMS/battery bus (Modbus-RTU to slave0xAA, owned bybattery.c); handle pp0x20009964, ctx0x20001A44, TX ring+0x330, RX ring+0x334. The two primitives were previously only scattered K&Rexterns; both preserve the same TXEIE/RXNEIE mask + DSB/ISB + implicit-r0-return ABI quirk asuart_send_byte/ssp_rx_byte, so they now returnint(the ring status).
Each ISR: on RXNE (+ RXNEIE, and — for usart3/4/5 — no SR error flag set) push DR into the RX ring; on TXE (+ TXEIE) pop the next TX byte to DR, disabling TXEIE when the ring drains. Crucially, all four also run the RM0430 error-clear sequence — for each latched SR error bit (PE
0x1/FE0x2/NE0x4/ORE0x8) read SR then DR to clear it (the CubeF4__HAL_UART_CLEAR_*FLAGidiom, captured here as a smallusart_clear_error_flaghelper). Two OEM quirks preserved: usart6 clears errors first (and omits the FE0x2clear), then gates RX on RXNE+RXNEIE alone, and re-loads the device handle fresh across the RX/TX halves; the other three sample SR/CR1 once and clear errors between the RX and TX phases. The initial Ghidra decompile elided the error-clear block as dead stack scratch — disassembly review caught it (reading DR has the observable hardware effect of clearing ORE), so it is reproduced for behaviour-equivalence. Also finishedmotor.c: decoded the body ofmotor_post_update_cb(0x08030994) —g_log_func("Reset F2806\r\n"), release the shared download timer slot (0x20000075), drop PB9 (motor reset). Build clean (text 1996, 0 warnings); all 7 functions adversarially verified instruction-by- instruction (PASS); 4 renamed + 6 prototypes, program saved. -
Motor-DSP (F2806x) firmware-update FSM →
src/motor.c(capstone —motor.cnow complete) — sourcedmotor_fw_update_fsm_step(0x08030FF4), the 14-state top-level engine that reprograms the motor controller via its C2000 SCI ROM bootloader, orchestrating the transfer pumps + transaction SMs of the two prior passes. Flow: enqueue an update-start inter-module message (state 0) → wait for the module ack (flag0x400000, state 1) → GPIOB reset/boot-pin sequence (PB90x200= reset, PB100x400= boot-mode select) to drop the DSP into its serial bootloader (states 3–4, with an RX drain that the OEM calls with a NULL sink) → settle + autobaud ('A'/'A', states 5–6) → upload a fixed 0x95E-byte handshake payload from flash0x0804463Cecho-verified (state 7) → second autobaud (states 8–9) → validate the staged motor pack at flash0x080A0000(magic0xAA55AA55, type byte0xA1, "Motorpcb Application: v%x.%02x.%02X (%s %s)" + " size %d bytes", state 0xA) → stream the C28x boot-stream payload (pack+0x28, length*(pack+0xC)-0x28, state 0xB) → success 0xC ("F2806-OK", bike-state0x1D, return 0) / failure 0xD ("F2806-err"/no log, bike-state0x1B, return 2). Progress logs F_init/F_reset/F_autobaud/F_upload/F_ready. Its control block is at0x20000075([0] = the download timer slot the pumps share, [1] state, [2] aux slot, [3] flag);download-ctx+0x3E(0x20000654) records whether the bike entered the "updating" state and gates the exit transition. Returns 0 done / 1 busy / 2 failed / 3 waiting.- String + arg fidelity: all 14 format strings re-read byte-exact from the OEM bin
(rodata, incl. the out-of-line
0x0804FCCC" ERROR SSP place" and0x0804FA1C" size %d bytes"); the case-0xA 6-arg version log resolved from the disassembly (r1=ver>>24, r2=(ver>>16)&0xFF, r3=(ver>>8)&0xFF, two stacked%spointerspack+0x10/pack+0x1C) since the decompiler dropped them. - Two-region body: the function's code is split by a literal-pool island (main
0x08030FF4–0x08031296, continuation0x080312E8–0x080313B0); both pools fully resolved. Created + named the previously-undefined 30-byte one-shot scheduler callbackmotor_post_update_cb(0x08030994, armed 200 ticks after the reset for both exits — body decode-pending). - Adversarially verified instruction-by-instruction (all 15 states PASS): the
tbhjump table, case-2-returns-3 vs default-returns-1, the case-1 doublescheduler_release, the magic/type checks, every GPIO pin/level, scheduler timeout, state-byte write and return value confirmed. Build clean (text 1996, 0 warnings); prototype set + 1 new function created, program saved.motor.c(8 funcs) now covers the entire mainware→motor-DSP update path; onlymotor_post_update_cb's body remains (a small leaf).
- String + arg fidelity: all 14 format strings re-read byte-exact from the OEM bin
(rodata, incl. the out-of-line
-
Motor-DSP (F2806x) transaction state machines →
src/motor.c— the handshake/ transfer layer thatmotor_fw_update_fsm_stepdrives on top of the four transport pumps (prior pass). All three share the same download context (0x20000654) at higher offsets and log viag_log_func(0x20009D98):motor_dl_send_verify_step(0x08030BFC) — sendcountbytes, verifying each is echoed back (the C2000 SCI bootloader echoes every byte): per-byte send (state 1) then receive+compare (state 2). Echo mismatch (non-final) logs "Fail 2 %d" + aborts; final-byte mismatch silently completes (OEM quirk). ctx slice: +0x20 state, +0x24 cursor, +0x28 remaining.motor_dl_autobaud_step(0x08030CEC) — the SCI autobaud lock: send'A'(0x41), expect'A'echoed → "Autobaud ok" / "Err Autobaud [%d]" / "Autobaud no answer". ctx +0x2C state.motor_dl_stream_block_step(0x08030D88) — 6-state length-prefixed block streamer with a running 16-bit additive checksum verified against the DSP's echoed checksum (per-block in state 4, periodic every 2048 bytes in state 5). Block length parsed from the first two streamed bytes (which are themselves also summed into the checksum). End-of-block = counter ==(length+3)*2. ctx +0x2D state, +0x30 cursor, +0x34 checksum, +0x38 counter, +0x3C block length.- String fidelity: all 14 log format strings re-read byte-exact from the OEM bin
(rodata 0x08050D20–0x08050DEC), and — per the standing discipline — every
g_log_funcargument resolved from the disassembly, not the decompiler (which drops the variadic register args): "Fail 2 %d"/"Err Autobaud [%d]" carry the r1-leftover value (count / received byte), and the two%04X %04Xstrings pass (received_u16, computed_checksum) in r1/r2. - Adversarially verified instruction-by-instruction (all 3 PASS): the case-3
tbhjump table, the no-double-advance length/checksum reads, thelen==0 && cnt>1returns-0 path, the (cnt-6)&0x7FF periodic test, and the per-case return values (mismatch returns the old state value 2; case4 clears +0x38 on match but case5 does not) all confirmed. Build clean (text 1996, 0 warnings); 3 renamed + 3 prototypes, program saved. Next pass: themotor_fw_update_fsm_stepbody (0x08030FF4, ~14 cases — GPIO reset pulses, pack-header parse, state orchestration; "Resetting motor" @0x08050DF0 onward) completesmotor.c.
-
Motor-DSP (F2806x) firmware-update transport → NEW
src/motor.c+include/motor.h(+ RX byte primitive →src/ssp.c) — sourced the transport foundation of the mainware→motor-controller firmware updater. The motor MCU is a TI F2806x (TMS320F28054F, C28x DSP); the OEM translation unit is literallysrc/F2806/f2806x.c(its assert-filename string @rodata 0x08050D0C), and the whole block (0x080309C4–0x08030FF4) reprograms the DSP over the inter-module ("SSPM") bus with a YMODEM-like block protocol undermotor_fw_update_fsm_step(0x08030FF4, already named). This pass took the bus RX byte primitive + the four non-blocking transfer pumps; the three transaction SMs (0x08030BFC/0x08030CEC/0x08030D88) and the top FSM body are the explicit next pass (string-heavy).sspm_bus_get_byte(0x08036664, wasFUN_) →ssp.c: the exact RX twin ofsspm_bus_send_byte— same device-handle pointer (0x20009924), but masks/sets RXNEIE (CR1 bit 5, 0x20) and reads the RX ring at bus-ctx slot +0xA54 (0x20002B3C) viaringbuf_get_byte; same ABI quirk (implicitly returns the get status, 0 = ring empty). Also called by the SSPM RX de-framerFUN_0803A0C0(counterpart ofsspm_bus_send_frame, future).- The four pumps →
motor.c:motor_dl_send_buf_step(0x08030B70, sends N bytes from a cursor viasspm_bus_send_byte),motor_dl_recv_buf_step(0x080309C4, receives N bytes viasspm_bus_get_byte),motor_dl_recv_byte_step(0x08030A50, one byte),motor_dl_recv_u16_step(0x08030ADC, little-endian u16). All four are one-byte-per-tick state machines that share one transfer context (0x20000654, disjoint slices: recv-buf +0x00/+0x04/+0x08, recv-byte +0x0C, recv-u16 +0x0D/+0x0E/+0x10, send-buf +0x14/+0x18/+0x1C) and one scheduler-timeout slot byte (0x20000075); they assert (not allocate) if the slot is stillSCHED_SLOT_NONE(0xFA) — the FSM allocates it first. Return convention 1 = busy / 0 = complete / 2 = timed out. Exact OEM assert line numbers preserved (0x161/0x197/0x1C9/0x1F5). Note:recv_u16sets state=1 at arm whereas the other three increment from 0 — reproduced exactly. - Adversarially verified instruction-by-instruction (all 5 PASS): ctx offsets,
assert line numbers, increment-vs-set, RXNEIE/TXEIE bit, deref levels, and the
0/1/2 return mapping all match. Build clean (
text 1996, 0 warnings); 5 renamed + 5 prototypes, program saved.
-
SSPM inter-module-bus TX path →
src/ssp.c+include/ssp.h— sourced the two outbound primitives for the second serial link (mainware↔battery/motor/ shifter, distinct from the BLE-coprocessor UART thatuart_send_bytedrives):sspm_bus_send_byte(0x0803662C, wasFUN_0803662C) andsspm_bus_send_frame(0x0803A008, previously only a namedexterninssp.c— and wrongly typedvoid: the disassembly returnsuint, 0 / 2).sspm_bus_send_byteis the structural twin ofuart_send_byte: device reg block via the pointer at0x20009924, TX-ring handle at fixed slot+0xA50into the bus context at0x20002B3C; it masks TXEIE (CR1 bit 7, with a DSB/ISB pair) around the ring push and re-enables it after re-loading the handle — same ABI quirk (implicitly returnsringbuf_push_byte's r0 status; 0 = ring full). It has two callers (sspm_bus_send_frameandFUN_08030b70, a scheduler-paced buffer drainer left for later), so it is exposed (not static) inssp.h.sspm_bus_send_frameis the SSPM-bus counterpart ofslip_send_frame: SLIP framing (0xC0delimiters,0xC0→DB DC/0xDB→DB DD) with a little-endian CRC-16 (poly0xA001, init0xFFFF) trailer; returns 2 only if the closing0xC0can't be queued (matching the OEMcbzat0x0803A0A6). Both adversarially verified instruction-by- instruction against the disassembly — faithful, no discrepancies. Build clean (text 1996, 0 warnings); renamed 1 + 2 prototypes, program saved. -
GPIO HAL deinit + EXTI ISR (CubeF4
stm32f4xx_hal_gpio.c) — completed the GPIO HAL surface insrc/gpio.c+include/gpio.h:HAL_GPIO_DeInit(0x08026990) andHAL_GPIO_EXTI_IRQHandler(0x08026AD4); Init/ReadPin/WritePin were already done (vendor-stock). Confirmed stock CubeF4 from the literal pools (SYSCFG0x40013800, GPIOA0x40020000, EXTI0x40013C00).HAL_GPIO_DeInitwalks pins 0..15: for each selected pin it checksSYSCFG->EXTICR[pos>>2]still maps the line to this bank (open-codedGPIO_GET_INDEX, A=0..H=7) before clearingEXTI->{IMR,EMR,RTSR,FTSR}+ the EXTICR nibble, then unconditionally returns the pad to reset (MODER/AFR/PUPDR/OTYPER/OSPEEDR). The OEM register-write order is not the textbook stock-source order — it is MODER(+0x00)→AFR(+0x20)→ PUPDR(+0x0C)→OTYPER(+0x04)→OSPEEDR(+0x08); the C reproduces that exact order and offset mapping.HAL_GPIO_EXTI_IRQHandleris the 4-line pending-bit demux (if (EXTI->PR & pin) { EXTI->PR = pin; HAL_GPIO_EXTI_Callback(pin); }). Added theGPIO_TypeDef/EXTI_TypeDef/SYSCFG_TypeDefregister blocks togpio.h. The application's overridingHAL_GPIO_EXTI_Callback(0x08038964, a real ~150-byte body, called directly by the ISR) was named + prototyped this pass but its body left for a future pass (stillFUN_-class work, not stock). Adversarially verified against the raw Thumb-2 disassembly — all three faithful: the non-textbook write order, every mask width (2-bit MODER/PUPDR/OSPEEDR, 1-bit OTYPER, 4-bit AFR/EXTICR), theposition>>2/(position&3)<<2EXTICR index math, the A..H index table, and the rc_w1 PR-clear semantics all confirmed. Build clean (text 1996, 0 warnings; gc'd like the rest — nothing roots it yet). -
UART HAL init/config/deinit (CubeF4
stm32f4xx_hal_uart.c) — sourced the UART bring-up path intosrc/uart.c+include/uart.h(docs/uart.md):HAL_UART_Init(0x08026CFC), the staticUART_SetConfig(0x08026AF0), andHAL_UART_DeInit(0x08026D5A). This is the foundation beneath all eightuartX_init/usartX_initwrappers (modem, e-shifter, display, inter-module buses). Confirmed stock CubeF4 from the literal pools: handle layout matchesUART_HandleTypeDefexactly (Instance@+0x00,Init@+0x04,Lock@+0x38,gState@+0x39,RxState@+0x3A,ErrorCode@+0x3C);UART_SetConfigreproduces the CR1/CR2/CR3 framing masks and theUART_DIV_SAMPLING8/16BRR math (OVER8 →(pclk*25)/(2*baud), frac(rem*8+50)/100, BRRmant<<4 | (frac&0xF8)<<1 | frac&0x7; OVER16 →/(4*baud), frac(rem*16+50)/100, BRRmant<<4 | frac&0xF0 | frac&0xF), with PCLK2 for the APB2 instances (USART1/USART6/UART9/UART10at0x40011000 + n*0x400) and PCLK1 for the rest. The OEMpclk*25numerator is a 64-bit dividend (__aeabi_uldivmod,0x08020AE4); modelled behaviour-equivalently as auint64_tdivide (the0x51EB851F/>>37÷100 magic written as/100). The runtime byte path stays the IRQ + ring-bufferuart_send_byte(already inuart.c); the blockingHAL_UART_Transmit/Receiveare not used by this firmware. Adversarially verified against the raw Thumb-2 disassembly — all three faithful, the OVER8/OVER16 branches confirmed not swapped, every bitmask exact. Externs kept out of scope:HAL_UART_MspInit(0x080333C0, per-USART GPIO-AF + RCC-enable + NVIC),HAL_UART_MspDeInit(0x08033740),HAL_RCC_GetPCLK1Freq(0x08027374),HAL_RCC_GetPCLK2Freq(0x08027394). Build clean (text 1996, 0 warnings). Triage note: the adjacent GPIO HAL is one pass from complete —HAL_GPIO_DeInit(0x08026990) andHAL_GPIO_EXTI_IRQHandler(0x08026AD4) remainFUN_(Init/ReadPin/WritePin already done). -
I²C HAL driver (CubeF4
stm32f4xx_hal_i2c.ctransfer layer) — sourced the entire blocking master/memory I²C driver intosrc/i2c.c+include/i2c.h(docs/i2c.md): 16 functions, the foundation beneath every on-board I²C peripheral (config EEPROM, STC3115 gas-gauge, LIS3DH, audio ampMAX9768, the two IS31FL3236 display drivers, the LED driver, light sensor). The centralHAL_I2C_Master_Transmit(0x08024760) alone has 26 xrefs across those drivers. Layers reconstructed from0x08023EEC..0x08025170: the flag-wait helpers (I2C_WaitOnFlagUntilTimeout/…MasterAddress…/…TXE…/…BTF…/…RXNE…+I2C_IsAcknowledgeFailed), the address/memory request helpers (I2C_MasterRequestWrite/Read,I2C_RequestMemoryWrite/Read), the blocking transfers (Master_Transmit/Receive,Mem_Write/Mem_Read), andHAL_I2C_Init/DeInit. The handle is the stockI2C_HandleTypeDef(pBuffPtr@+0x24, XferSize@+0x28, XferCount@+0x2A, XferOptions@+0x2C, PreviousState@+0x30, Lock@+0x3C, State@+0x3D, Mode@+0x3E, ErrorCode@+0x40); the flag tokens decode to the exact CubeF4I2C_FLAG_*(reg<<16)|bitvalues (BUSY=0x00100002,ADDR=0x00010002,ADD10=0x00010008,BTF=0x00010004,XferOptions=I2C_NO_OPTION_FRAME=0xFFFF0000).HAL_I2C_Init's clock math is the standardI2C_FREQRANGE/I2C_RISE_TIME/I2C_SPEED(the0x431BDE83÷1e6 and0x10624DD3÷1000 magic-multiplies modelled as the plain divisions they implement; PCLK1 ≥2 MHz std / ≥4 MHz fast guards; the(field+1)&0xFFF==0→1and(ccr&0xFFC)==0→4clamps).i2c3_handle_init/_deinitwere refactored onto the struct andHAL_I2C_Init/DeInitare now real bodies (the externs are gone). The MSP/clock helpersHAL_I2C_MspInit/MspDeInit(GPIO+NVIC+RCC,0x0803C69C/0x0803C820) andHAL_RCC_GetPCLK1Freq(0x08027374) stay externs — a separate MSP/RCC cluster. Adversarial verify (one read-only pass over all 16 vs the disassembly) confirmed 15 faithful and caught one access-fidelity slip: the ADDR-clear inI2C_RequestMemory{Write,Read}must read SR1 then SR2 (the C had read only SR2 with a wrong comment) — fixed to emit both volatile reads. Confirmed subtleties:Master_Receive's pre-loop N≥3 setup sets ACK in anelsebranch, butMem_Read's deliberately does not (ACK already set inI2C_RequestMemoryRead); the RXNE wait's timeout is unconditional (noHAL_MAX_DELAYescape) and its STOPF path leaves ErrorCode untouched;I2C_ERROR_WRONG_START(0x200) is written on a START-still-set SB timeout. Build clean (text 1996, 0 warnings; gc'd like the rest — nothing roots it yet). -
LIS3DH accelerometer driver — sourced the whole motion-sensor driver into a new
src/lis3dh.c(docs/lis3dh.md): 26 functions via a resolve → translate → adversarial-verify workflow (9 agents, both translate + verify per group). The ST LIS3DH hangs off I2C3 at 8-bit addr0x33(7-bit0x19); a per-device vtableg_lis3dh_dev@ SRAM0x2000838C({write 0x0803D098, read 0x0803D074, wait 0x0803D070}+ WHO_AM_I scratch) abstracts the blocking HAL transport (HAL_I2C_Mem_Read/Write(.., 0x33, reg|0x80, 1, .., 50ms)). Layers: 3 transport leaves, 3 vtable thunks (0x0802E434/444/44E, confirmed to propagate the callee status), 15 register helpers (0x0802E45E..0x0802E782— CTRL_REG1-5 / REFERENCE / WHO_AM_I / INT1_CFG/SRC/THS/DURATION, all read-modify-write with thewait→return 3shape), and the public APIlis3dh_accel_init/config_motion_int/powerdown/int1_clear(+ privatelis3dh_int1_read_source).config_motion_intis the textbook motion-wake setup (HPF on INT1,I1_IA1, ±2 g, 100 Hz, OR-of-X/Y/Z high events);status_processarms it(0,6)high /(0,0x20)low. Verify caught one real refinement — theelse-branch INT1_CFG mask is& 0xD7in the asm (not the decompiler-folded& 0x57); the asm-faithful(cfg & 0xD7) | 0x02then unconditional& 0x7Fis used. Correction:lis3dh_accel_initcallsNVIC_DisableIRQ(0x080270FC,NVIC->ICER), i.e. it masks EXTI IRQ0x48/0x49during the probe —hardware.mdpreviously said "enables", now fixed. Also resolved the adjacentcharge_time_estimate_reset(0x0802E40C, was grouped by flash proximity but NOT accelerometer): re-arms the 10 s BMS poll (G_STATE[0x1d]) and forces the charge-time estimate at app-ctx+0x3FEto the0x8300(-32000) invalid sentinel — sourced →states.c. Build clean (text 1996, 0 warnings). -
Modem AT response/command callback layer — sourced the last un-decoded layer of
modem.c: the 32.build_cb/.handle_cbtargets of the flash AT-script table (0x0802F1DC..0x0802FD68) →modem.c(resolve → reconstruct → adversarial-verify workflow, 14 agents; build clean,text 1996). The layer is four parsing primitives (modem_skip_to_cr/modem_skip_to_space/modem_extract_field/modem_at_response_copy), the per-command response handlers (CGMI/CGMM/CGMR/CGSN/CIMI identity → the0x20009CC0..0x20009D00scratch block; CCID→g_modem_iccid; CPIN/CSQ/CREG/CMGF/UPSND/UUHTTPCR/CPMS/CMGL status; UGSRV/UGAOP/UPSD operator/AssistNow/APN checks; UULOC location-report builder), and the snprintf command builders (CPIN=/CMGS/CMGR/CMGD/SMS-body/ UPSD/UHTTPC/UHTTP/UGAOP). Helpers resolved:FUN_08021BDC=strstr,FUN_0802133C=strchr,FUN_0802181C=bounded_strncmp(0 == equal). The CMGR handler feedsmodem_sms_dispatch_command(0x0803D668) — the inbound-SMS remote-command interpreter (#<code>*<cmd>→ unlock / factory-reset / state / location / bell), the SMS half of the anti-theft surface (named, body not yet sourced). Adversarial verify caught 2 cursor-threading bugs (CMGR/UPSDpassed the response start to the skip primitives instead of threading thestrstr/extract_fieldcursor — fixed). Full callback + scratch-global map indocs/modem.md. Triage note: the remainingFUN_are mostly newlib/libgcc (the0x08020xxxregion) and HAL statics; the next coherent app cluster is the0x0802E40C..0x0802E782I²C register-device driver ({write,read,poll}vtable). -
Sleep + reset/factory-reset state machines — sourced the two long-deferred
status_process/loop control functions intostates.c(resolve → reconstruct → adversarial-verify workflow, 10 agents, 156 symbols resolved; build clean,text 1996). A boundary surprise drove the framing: decompilingreboot_restart_task(0x08038A68) returned a glued ~250-line state machine with a bogusunaff_r4 * 0x1000000base. The cause was a false fall-through — Ghidra hadn't markedFUN_08038A14no-return, so it ran the literal pool at the end ofreboot_restart_taskstraight into the next function.FUN_08038A14isNVIC_SystemReset(DSB;SCB->AIRCR = 0x05FA0004 | (AIRCR & 0x700); DSB; spin) — marked no-return, after which three distinct functions resolved cleanly:enter_stop_mode(0x080382D0,void(uint8_t reason)) — low-power STOP entry / wake-arm sequencer. Logs"EnterSTOPMode %d min "+ a per-reason wake string (Wake:All/NoMems/RST/Shipping/No bat/ERROR), runs the peripheral de-init cascade (8 UART veneers, I2C×2, SPI, ADC, an AHB1 periph), parks all of GPIOA..H to analog (enable→Init→disable the AHB1ENR clocks), arms a per-reason EXTI wake-pin map on GPIOC/GPIOD (GPIO_MODE_IT_FALLING/RISING) + the RTC wakeup timer (skipped for reason 6 "Shipping"), thenenter_low_power_wait(PWR STOP + WFI — sleeps here) and on wake re-inits the clock tree (warm/cold from the0x20000000marker) andsystem_software_reset()s. The per-reason sleep duration + wake mask live in one runtime RAM struct@0x20000094(slot[reason] u32 ms; its low u16 = RTC wake seconds;+0x24= write-through clear-ptr). Two critical reconstruction bugs were caught by the adversarial pass and fixed: the wake-mask read used stride 2 instead of the OEM's stride-4ldrh [base,reason<<2], and a stray pad pushed the clear-ptr off its+0x24offset.reboot_restart_task(0x08038A68,void(void)) — the actual function: a 5-line reboot wrapper (clear warm marker → cold boot, log"NVICReset", 20 ms delay,NVIC_SystemReset()). Armed as a scheduler callback byconsole_cmd_reboot, the OAD-failed path, andfactory_reset_sm_step.factory_reset_sm_step(0x08038A90,void(uint8_t *ctx)) — the 6-state USER/factory-reset & power-cycle machine ticked frommain()(g_reset_sm@0x20006E44:[0]=state,[1]=sub-step). Drives "USER Reset" → BLE notify 0x11D →gas_gauge_reset/settings_factory_reset/15-word EEPROM save → NVIC reset, with a state-5 hardware-reset cascade (BLE/eShifter/"\nReset BMS\r"over the inter-module bus/Motor). Verdict: faithful.
~27FUN_callees named in Ghidra this pass (prototypes + plate comments, program saved):system_software_reset(a second inlinedNVIC_SystemReset@0x080382AC, no-return),uart_handle_deinit_0..7+ the shareduart_handle_deinit/HAL_UART_MspDeInitworkers,i2c_handle_deinit/spi_handle_deinit/adc_handle_deinit/ahb1_periph_handle_deinit, CMSISnvic_set_priority/nvic_enable_irq/nvic_clear_pending_irq,enter_low_power_wait,systick_irq_ disable/enable,set_wakeup_done_flag,rtc_set_wakeup_seconds/rtc_wakeup_ timer_disable/rtc_wakeup_timer_set, andreset_ble_timeout_cb(0x08038A38, newly carved). The logger object@0x20009D98is confirmed a 3-slot fn-ptr table ([0]=printf=g_log_func,[1]=putchar,[2]=alt-formatter). Note kept for a future pass:shifter_usart3_reinit(0x080338B4, already sourced inshifter.cas a "re-init/flush") is actually the 3rd UART de-init veneer — a misnomer to revisit, left as-is to avoid destabilising existing verified code. -
Backup-code investigation + name correction — chasing "what is the default backup code?" exposed a misnomer: the leaf I'd called
backup_code_init_default(OEM0x0803FAC0) does not touch the backup code. It seeds the three sound-group volume-tier masks atctx+0xF4/F8/FC(console_cmd_show's "Group low/medium/high %08X") to{0, 0x383F33FE, 0x47C0CC00}— medium∣high partition sound bits 1..30 disjointly. Renamed tosound_groups_init_default(Ghidra +app.c/main.c, prototype + plate comment set, literals labelledg_dwSoundGroup{Medium,High}Default, program saved); the old extern inapp.calso carried a wrong OEM address (0x0803FC50is interior toregion_speed_preset_table_load). The real backup code is theuint16atctx+0x100: a 3-digit owner code (value = d0 + d1·10 + d2·100, 0..999) programmed over BLECMD_BLE_SECURITY_BACKUP_CODE(handler0x080349ac, packs payload[0..2]); entered at the bike via horn presses (states.c"No backupcode"). Factory reset writes0x00FF= "not set" sentinel — there is no factory-default backup code; the bike ships with it unset (console_cmd_show→ "backupcode not set"). Documented indocs/hardware.md(g_ctxfield map). Build clean,text 1996. Follow-up: decoded the rest of the config block from theshowformat strings and rewrotesettings_factory_reset(app.c) from flat magic-offset writes to a documentedbike_config_toverlay — sound groups, backup code, dark threshold (lux), sound volumes low/med/high+0x105/6/7, and the per-region pedal-assist "moment" presets (+0x10Eengage /+0x126release,[4][3]u16 in 0.1 km/h) now read by name, with_Static_asserts pinning every field to its OEM offset. Behaviour-identical (final memory state + persist call unchanged),text 1996. -
Named-leaf sourcing pass — the 25 leaf functions the BLE/app/state modules still called as opaque
externs (named in a prior pass, bodies undecided) were decoded + adversarially verified (a decode→refute workflow, 50 agents, each cross-checking the live disassembly + OEM bytes), then 24 sourced into their home modules (build clean throughout,text 1996):- ble_read telemetry surface —
ble_get_charge_plug_state+ the 0x60-byteble_build_testmode_versions_blob→ble_read.c(its read-surface block now#includes the home headers and drops the opaque-extern thicket);charge_level_adc_get+hw_version_lookup(16-entry HW-ID divider table @ flash0x08044F9C, ADC ctx0x20000914) →sensor.c;gpio_pc0_is_low/gpio_pc1_is_low(PC0/PC1) →gpio.c;ble_get_led_channel_state(g_lights0x20006DC0, byte +0x0D per 0x14 channel) →lighting.c;telemetry_map_clamp(linear map+clamp) →util.c. - bike-state getters →
app.c(read the FSM object0x20000029+4):bike_status_coarse_get,bike_state_is_standby,ble_lock_state_get(0x28–0x37 pin-lock window),ble_unlock_state_get; plusapp_ctx_clear_field_328(0x200083A8+0x328) andclear_flag_00e5(SMS flag0x200000E5). - RTC helpers → new
rtc.c/rtc.h:rtc_now_epoch_seconds/rtc_set_from_unix_time— thin wrappers; the OEM delegates the calendar↔epoch date math toFUN_08037F9C/FUN_08038110and the register I/O to the RTC HAL, all kept as to-be-decoded externs (RTC handle0x200099E4). - outbound-message enqueue
maybe_enqueue_tx_message→ssp.c(16-slot × 24-byte table @0x20007E14, rolling handle @0x200000C8); the verify caught the collision path advancing to the next slot (not a same-slot retry). - BLE/state helpers →
ble.c(count_active_2bit_groupsstatic,sspm5_tx_timeout_cb,post_request_with_arg@ reset-SM block0x20006E44) andstates.c(announce_records_reset@0x20009360,display_announce_enter,display_timeout_timer_set,lock_poll_timer_arm). - Caught:
ble_build_testmode_versions_blobis called withr0 = 0from the 0x5551 read yet stores*out_crcunconditionally (the OEM relies on the write to flash-alias address 0 being benign) — guarded withif (out_crc)to keep the observable behaviour without UB. Deferred:config_persist_dual_bankmemcpy's 0xC0 stack bytes via&stack0x00000000(a variadic stack passthrough irreconcilable with the existing 4-arg callers) — left as a named extern. All literals/tables/sentinels re-read from the OEM binary; ARM-unsigned-char sentinels (0xFA slot,(uint8_t)(state-0x28)window) preserved.
- ble_read telemetry surface —
-
Console/diag sourcing pass — the three diagnostics leaves the console/BLE surfaces still called as named externs were decoded + adversarially verified (a decode→refute workflow, 10 agents, every format string + field offset re-read from memory and cross-checked against the live disassembly), then all 3 sourced (build clean,
text 1996):app_log_sink_enable(0x080298DC) →log.c/log.h: a single byte store of 1 to the "log to APP sink" enable flag at SRAM0x200001D6(read by the log-upload state machine). Labelledg_log_app_sink_enablein Ghidra.log_buffer_dump(0x080296B8) →log.c/log.h: walks the SRAM ring (LOG_CTRL@0x20037000read→write cursor, wrap end0x2004FC00→payload0x2003700C), kicking the watchdog per byte, reassembling lines into a 256-byte buffer (uint8_t index → over-long lines wrap, OEM behaviour). Each line's leading decimal is its epoch; when non-zero it is reformatted as">DD/HH:MM:SS "+ the message from the first space on (viartc_epoch_to_calendar- the OEM
strcspn), else echoed raw">%s". Reusesrtc_calendar_t+rtc_epoch_to_calendar, now promoted fromrtc.cintortc.hso both modules share them. Two helper callees named this pass:parse_leading_decimal(0x08020DDC, astrtol(s,NULL,10)wrapper) andfirst_index_of_set(0x080216FC,strcspn) — kept as externs (OEM ships its own copies).
- the OEM
console_battery_dump(0x0804175C) →console.c: the one-shot 1 s scheduler callback armed byconsole_cmd_battery; releases its slot then prints ~50 BMS telemetry fields from the session-context shadow (CTXB+0x3F2..+0x49E). Field encodings verified field-by-field: plain%dcells are s16; the three temps (+0x43C/+0x43E/+0x440) subtract 0xAAB (0.1 K → 0.1 °C);I/+0x3FE is ×10; HW_VER/FW_VER (+0x406/+0x408) print hi.lo (%X.%02X); FAULT/UBAT/FSR are raw u16. The PDOCP/+0x498 + PDSCP/+0x49A format strings live in a separate rodata island (0x080507C8/0x080507D4), not the contiguous0x080542DCblock. Redundantexterns dropped fromble.c/console.c(both#include "log.h").
-
Remaining named-leaf sourcing pass — 14 leaves decoded + adversarially verified (decode→refute workflow, 45 agents, 3 lenses each on the trickier ones), 15 sourced (build clean,
text 1996); 2 deferred:- SSP outbound pumps →
ssp.c:tx_table_handle_in_use(replaces its stub),sspm_tx_queue_pump(inter-module bus, the 16×0x18 table @0x20007E14+ timer pair @0x200000C8, bus sendersspm_bus_send_frame@0x0803A008) andssp_ble_tx_queue_pump(BLE SLIP queue @0x20008A40, timer pair @0x200000F0, BLE-reset GPIOC pulse). Verify caught a candidate inventing a pointer var for the0x20009044(==&queue[0x604]) frame-staging address — it's an address immediate, passed directly. - RTC date math →
rtc.c:is_leap_year/days_in_year+ the two convertersrtc_calendar_to_epoch(0x08037F9C)/rtc_epoch_to_calendar(0x08038110) +rtc_fill_time_fields(0x080380A4). The OEM magic-reciprocal divides are modelled as the plain/60,/3600,/86400,/7,/100,/400they implement (math lens hand-traced an epoch example); the days-in-month[2][12]table @0x0804F340materialised ask_days_in_month; the 2-digit-year(char)year+'0'byte-truncation quirk reproduced. HAL getters namedhal_rtc_get_time/hal_rtc_get_date. rtc.c fully rebuilt to host all of it. - misc:
wwdg_apb_clk_disable(clears RCC_APB1ENR.WWDGEN) →watchdog.c;stc_gas_gauge_set_run(STC3115 MODE reg bit0;stc3115_read_reg/write_reg) →sensor.c;motor_get_timer_cb(~12-field motor telemetry dump) →console.c;shifter_mode_command_dispatch(8-case OAD/mode dispatch driving G_STATE+4 + the update SM) →shifter.c;region_speed_preset_table_load(3 region preset tables materialised) →app.c. config_persist_dual_bank(the previously-deferred one) →flash.c, now fully resolved: it takes 4 register words + a trailing 0xC0-byte by-valuestruct boot_cfg_block(promoted toflash.h) and writes both flash banks (A0x08008000/ B0x0800C000) viaflash_config_bank_write. All 12 call sites inble.c(9) +console.c(3) rewritten from the old 4-arg + dead-memcpyplaceholder to pass the payload struct;settings_factory_resetalso sourced (→app.c) with the same call; main.c's local struct/extern folded intoflash.h.- Bonus bug fix:
console_cmd_motorstatusarmed the motor poll onSCHED_SLOT_TELEMETRY(0x20000110) but the OEM (andmotor_get_timer_cb) useSCHED_SLOT_REC(0x2000010E, verified at pool0x08042F08/0x08040EB0) — corrected. Deferred (status_process-class, not leaves):— done, see the "Sleep + reset/factory-reset SMs" entry at the top of this log. (The "unresolved ctx base" of theenter_stop_modeandreboot_restart_task0x08038A68decompile turned out to be a false fall-through artifact:reboot_restart_taskis a tiny 5-line NVIC-reset wrapper and the 6-state machine is the adjacentfactory_reset_sm_step@0x08038A90, whose base is a cleanctxparameter.)
- SSP outbound pumps →
-
display.c— the LED-matrix display engine, now SOURCED (33 functions, faithful C; fan-out transcribed + adversarially verified). The bike's signature dot-matrix panel: two IS31FL3236 LED drivers on I2C2 (halves @ device 0x60/0x66, IS31FL373x0xFE/0xC5unlock +0xFDpage-select protocol), a dual-half framebuffer ing_request_ctx(0x20008230: half A+0x01, half B+0x99, flags+0x130/+0x131/+0x132), the bit-banged glyph/number/bar draw API (3-bit LUT + 5×7 glyph ROM at flash0x0804F358), the I2C-DMA frame transmitter with display-freeze bus recovery (led_matrix_transmit_step, logs" ERR dsp freeze"), and the ~40-case display-mode presenter (display_mode_sm_step). Full writeup →docs/display.md. Name fix:dsp_recovery_telemetry_pump→led_matrix_transmit_step("dsp" = display). Assembly lessons: hoisted the shared LUT + glyph ROM to single file-scope tables (the agents' per-function inlines collided / shadowed);display_module_initreturnsdisplay_panel_reset()'s status (r0 survives the flag stores) — the boot path branches on it;uint→uint32_t, local GPIO-init struct for the bus-recovery path. Request descriptors materialized: the 38 distinct flash animation/glyph descriptors thatdisplay_mode_sm_step/matrix_draw_speedfed tomaybe_set_pending_request/display_request_setas bare magic addresses are now real C data indisplay_requests.c(named symbols, not hex); the three string args still passed as raw addresses ("slow_show_speed_tmr","NAK\r\n"," ERR dsp freeze\r\n") are now string literals. -
display_requests.c— LED-matrix display request descriptors (data only, no functions): byte-faithful C copies of the 38 flash rodata descriptors the matrix render/overlay pipeline consumes. Format decoded fromled_matrix_render_frame_region+matrix_glyph_src_addr/_frame_delay: auint32array[origin_col, width, frame_count, frame_count×delay, frame_count×width packed-3bit-pixel words], length3 + frames·(1+width)words. Extracted from OEMmainware_1.07.06.bin(image base0x08020000); length formula verified across the whole contiguous0x0804c104→0x0804da64chain. 34,092 bytes total; each descriptor labelledg_disp_req_<addr>in Ghidra. (gc-sections out of the linked image like the rest — build stilltext 1996.) -
lighting.c— the front/rear lamp engine + ambient light sensor, now SOURCED (8 functions, faithful C; fan-out transcribed + adversarially verified). Three TIM-PWM lamp channels (CCR1/2/3 viag_lights@0x20006DC0) with a ±1 fade engine and ten table-driven flash patterns (light_pattern_step/light_pattern_action_apply); the ambient sensor read path over I2C2 (device 0x20, reg 0x50) with retry +" ERR CM2323"fault. Full writeup →docs/lighting.md. Verify catch: the fade-step callback was transcribed with swapped operands AND a dropped argument — the OEM calls the PWM callback with the just-updated brightness (cb(ch[0x0D])) and the fade-down test istarget < current; both fixed. Note:light_tick_update(0x080371E8) is a console↔ shifter/battery UART bridge, not a lamp function — left named, out of scope. -
main.c— the application capstone, now SOURCED (4 functions, faithful C, adversarially verified):main(0x0803DEA8, super-loop),boot_init_cold(0x0803DDE0),boot_init_warm(0x0803DADC),mainware_boot_init_sequence(0x0803FC94). Full writeup →docs/boot.md. The OEM's own assert filename for this TU is literally"src/main.c"(rodata0x08053320). Entry relocatesSCB->VTOR=0x08020200, dispatches warm (*0x20000000==0x55AA55CF, HSE+LSE) vs cold (HSE+LSI) clock setup (PLL HSE/6×96/2, FLASH_LATENCY_3), runs ~30 peripheral inits (8 UARTs, I2C2, TIMs, ADC1, RTC, CRC, DMA, WWDG, scheduler), thenmainware_boot_init_sequence(banner"ES3 v%d.%02d.%02d", GPIO rails, I2C device self-test HDC1080/STC3115/LIS3DH/MAX9768 with a fault-counted retry loop, state-record + config-schema defaults, SIM-source detect, model string). The infinite loop ticks lights → modem SM → shifter/BMS Modbus → BLE/SSP bridges → motor recovery → LED matrix →status_process→ telemetry →subsystem_update_smeach pass. Link:mainis weak so the startup stub stays the entry (closure not yet rooted) — build clean,text 1996. 31 strings byte-exact; the ~74 callees named in Ghidra (one verify-pass correction:0x080398CEisstc3115_wake, not the agents' "accel_disable" — proven by its caller gatingstc3115_fuel_gauge_init). RCC names de-rotated:0x0802643C=OscConfig,0x08027208=ClockConfig,0x08026064=PeriphCLKConfig. -
battery.c— the battery / BMS Modbus driver, now SOURCED (18 functions, fan-out + adversarially verified;docs/battery.md). Modbus-RTU master to BMS slave 0xAA (func 3 read / func 6 write, CRC-16 poly 0xA001) — the master side of thebatterywaredecomp, cross-validated. Core:bms_modbus_read/bms_modbus_write(PDU builders),bat_modbus_master_step(byte SM: request+CRC → response+verify),bat_modbus_txn_pump,modbus_bat_service_step(super-loop tick),bms_telemetry_unpack(register → app-ctx cache: reg N →*(uint16_t*)(g_app_ctx+0x3F2+N*2), BE u16; serial reg 0xB → ctx+0x408),battery_telemetry_step(the ~16-state pack lifecycle: detect→bring-up→charge→ reset→off),battery_state_process,battery_charge_display_step(Set power state to %s),battery_on_detect_step/substate_advance,batteryware_update_*. Ctxg_bat_modbus_ctx@0x20006E90(frame+0xE7C), BMS state @0x200000E7, update record @0x20008A00. GPIO: PC10 present / PC4 charger (GPIOC), PD1 sense (GPIOD), PB5 BMS-reset (GPIOB). The verify pass caught a badly-mangledbattery_telemetry_step(its agent scrambled the per-case log strings + GPIO bases and dropped case-0xF'sbms_modbus_write); re-decompiled + rewritten faithfully (strings resolved exactly, incl. the leading-space" ERR battery FAULT PIN"). Shared bus CRC/TX helpers named, extern (futurebus.c). Build clean (gc'd,text 1996). Charge/fault leaf cluster now SOURCED (7 more funcs, +the 18-row charge-policy table @ OEM0x0804F47Cmaterialized asg_battery_charge_table):battery_charge_lookup(SoC/temp/flags → rate+mode),battery_charge_complete_watchdog(2 s→10 s full-charge timeout),battery_set_charge_mode(writes app-ctx +0x341 via the*G_BATWARE_UPDpointer),battery_telemetry_state_get(G_BAT_STATE+3),battery_fault_warning_report(BMS fault/warn → state-flags+0x3B8, PC4 charger gate,"BMS fault/warning 0x%04X"),bus_queue_peek_status,status_clear_pulse_flag. All DAT bases + the charge table + the two log strings re-read from the OEM bin (the decode agent's watchdog limit-25511was wrong → actual -24999).scheduler_slot_get_remainingsourced intoscheduler.c(raw-counter twin ofscheduler_slot_is_idle). Build clean,text 1996. -
shifter.c— the shifter / drivetrain Modbus master, now SOURCED (37 functions, fan-out + adversarially verified;docs/shifter.md). The master side of the flagship shifterware: Modbus-RTU to the eShifter / MT-shifter (slave 0x20, func 3/6/0x10, CRC-16 poly 0xA001) over USART3 (9600). Layers: the shared frame-ring queue (bus_queue_init/push/pop/reset/peek, the same primitivesbattery.cexterns — now defined here, somodbus_shift_submitfinally has a body), the byte-level RTU engine (shifter_modbus_rtu_step+shifter_crc_*+ locked USART3 byte I/Oshifter_uart_tx_byte/_buf/_rx_byte, RX-flush watchdogshift_rx_flush_timeout_cb), the transaction pump (shifter_modbus_pump→shifter_response_unpack, which decodes func-3 replies into the session ctx +0x520.. and feeds theshiftdebug"MBS 0x%04X 0x%02X"stream), the link monitor (modbus_shifter_link_monitor, super-loop entry frommain), the ~18-case drivetrain control SM (shifter_control_step: power-up, ID/HW handshake into ctx+0x520 = 0x200 std / 0x201 MT, auto-shift against the per-region speed-threshold tables at ctx+region*6+{0x10e..}/{0x126..}, manual shift-button path, MT zeropos calibration; gear actuatorshifter_send_gearfunc-6 reg 2;shifter_seq_status_poll_step18-state register sweep), the ~20-case OTA update SM (shifter_update_sm_step: probe →pack_validatethe staged image at flash0x08010000→ stream 0x20-byte blocks via func-0x10 reg 0x82 → poll CRC/erase answer ctx+0x620 → reset + version readback → persist to EEPROM), the pre-check gateshifter_firmware_update_step, the SM accessors (shifter_sm_get_step/set_step_3/_10/_13,shifter_get_active_flag,shifter_update_request/_status_get), and the console dumps (shifterstatus_dump_v200/v201). SRAM:g_shift_modbus_ctx@0x20005E3C,g_modbus_crc@0x2000008C(+2 = RX-timeout slot0x2000008E),g_shifter_sm@0x2000001C,g_shifter_ctx@0x2000019C,g_bus_ring_slots@0x20006E00,g_usart3_handle@0x20009824,g_usart3_rings@0x2000094C; the SMs'ctxparam = the session contextG_APP_CTX@0x200083A8(=g_app_state.ctx_sub). Verify catch (critical): the adversarial pass found the two big SMs had their dense per-case log strings shifted by a literal-pool slot (and onehdr[2]that must behdr[10]— a real stack overflow, sincepack_validatewrites a 0x28-byte header). I re-resolved every literal byte-for-byte from the OEM image and found the globals-agent table itself was wrong in 26 places (incl. the control-SM MT-case" ERR MS overflow"the verifier hadn't flagged); all fixed against ground truth.bus_queue_peekreturns the queued-frame count (not a frame word). 23FUN_*renamed in Ghidra + 10 SRAM/cb labels; build clean (gc'd,text 1996,shifter.o= 9551 B code). -
Placeholder-decode / naming pass — the sourced modules called ~44 leaf helpers still left as
extern FUN_xxxxxxxxplaceholders. A fan-out workflow (5 groups + adversarial verify) decoded/classified/named them; allFUN_references acrosssrc/were then replaced with the real names (in source + Ghidra), build-neutral (text 1996). Corrections the verifier caught:0x080398b8isstc_gas_gauge_set_run(STC3115 MODE reg, set bit 0) — a prior session had mislabelled itaccel_enable(the suspicion noted inboot.mdis now confirmed);0x080314a4/0x080314b4arewwdg_apb_clk_disable/wwdg_apb_clk_enable(RCC_APB1ENR.WWDGEN bit 11 @0x40023800), notflash_cache_disable/enable(the regs are RCC, not FLASH —update.ccalls them before blocking flash/stop). Reconciled collisions where the decode invented names already in use:0x0803c5f0/5fc/608=obj_set_field34/obj_set_field38/led_channel3_set_brightness(lighting.c);0x0803c1c0=led_driver_enter_shipping_mode,0x0803b76c=display_request_clear,0x0803bec8=matrix_draw_icon,0x0803b2c4=display_send_init_cmd(display.c);0x08037aac=light_sensor_read_stepand0x08037160=charge_level_adc_get(states.c/lighting.c — kept);0x08040350/0x08040368aregpio_pc0_is_low/gpio_pc1_is_low(mask 1 = PC0, mask 2 = PC1). Newly named bespoke leaves includebattery_charge_lookup(18-row charge-policy table @0x0804f47c),battery_fault_warning_report,battery_telemetry_state_get,bus_queue_peek_status,stc_read,rtc_set_from_unix_time/rtc_now_epoch_seconds,config_persist_dual_bank,watchdog_init,hw_version_lookup,announce_records_reset,display_announce_enter,display_timeout_timer_set,lock_poll_timer_arm,shifter_mode_command_dispatch,sspm5_tx_timeout_cb,ble_get_charge_plug_state,ble_build_testmode_versions_blob,ble_get_led_channel_state,telemetry_map_clamp,count_active_2bit_groups,post_request_with_arg,app_log_sink_enable,clear_flag_00e5,app_ctx_clear_field_328,lis3dh_powerdown,scheduler_slot_get_remaining,status_clear_pulse_flag.0x08024d2crecognized as vendorHAL_I2C_Mem_Write. Only 6FUN_refs remain insrc/— all CubeF4 HAL I2C/memsetvendor stubs. Bodies of the newly-named bespoke leaves are decoded (workflow output) but not yet sourced into their home modules — a per-module follow-up. -
Per-state engine (
status_process0x0802AAF8, ~14 KB) — decoded + documented (docs/status-process.md), the bike's central behaviour engine ticked every super-loop: dispatches on the bike-state byte and runs each state's logic (read LIS3DH accelerometer / wheel / buttons / BMS / charger, drive lights P1–PC + sound + motor/shifter, manage LiPo charge & CPU-stop/sleep, run the alarm/kick-lock/PIN flow + GSM tracking, finalize OTA/diag viaNVICReset). ~80-callee surface; moves the state viamaybe_set_state_if_unlocked. Now SOURCED as full faithful C →src/states.c(~2300 lines: a common prologue + a 62-caseswitchonG_STATE[4], cases 0..0x3D = thealarm_state_namecodes), via a region-fan-out transcribe→verify workflow (setting the 1-param prototype lets the decompiler render the whole function; the 62 cases were split across 9 agents, assembled, then adversarially verified — the verify pass caught 3 transcription slips: a deref-vs-address arg in case 9 and two copy-pasted log strings in cases 0x1b/0x26, all fixed). Model: the recurring DAT aliases collapse toctx(param),G_STATE(signed char *@0x20000029, state byte[4]),G_CLK(@0x200001D8) andg_log_func; ARM unsigned-charmade the== -6/-0x12slot sentinels needsigned char. Build clean (gc'd,text 1996). The logic counterpart toble-commands.md/console.md(inputs) andstate-machine.md(positions). -
update.c— OTA orchestrator (subsystem_update_sm0x08031900) now SOURCED as full faithful C (771 lines) via source→verify workflow, with all 62 log strings byte-exact and every callee resolved to a named extern (build clean, gc'd). The multi-subsystem firmware-update state machine (~22 states): updates all five firmwares from one BLE PACK package —mainware.bin(self → shadow flash0x08060000+NVICResetreboot to bootloader, "wait for BMS shutdown"),motorware.bin(0x080A0000),shifterware.bin(0x08010000),batteryware.bin/powerbank (Modbus 0xAA,0x080C0000),bleware.bin(OAD). Control structg_update_sm@0x20000760, slotsg_update_slots@0x20000079. Target-type map (ctx[0x32C+i]): 1=bleware, 2=mainware, 3=motorware, 4=shifterware, 5=batteryware. 8 helper callees named;memcmp(0x08020E60) vendor. Per-target verify withpack_validate+ version compare. Doc:docs/ota.md. -
ble.c/ble_read.c— the BLE app-command surface, both dispatchers sourced (were named+documented).ble_cmd_dispatch(0x08033970, ble.c) = the write/command switch (~400x55xxGATT-write cmds + low provisioning ids);ble_read_request_dispatch(0x08034D20, ble_read.c) = the read/ telemetry twin (~50 char ids the app polls). Decoded by fan-out + adversarial verification against the live disassembly: the verify pass caught the ctx-holder-vs-ctx trap (cmd0x5536reads the eShifter-busy global at0x20000948= holder+4, NOT a ctx field), confirmed the0x11AMAC-stringsnprintftakes 3 args (real fmt"%02X%02X%02XDeBug", no OEM bug), and the0x55A2testmodememcpyis 0x28 B. ctx model =*(uint8_t**)0x20000944; both loggers (g_log_functable[0]/[2]) collapsed tog_log_funcper the console.c convention. Control flow / offsets / helpers / state transitions are verified. Debug-log strings are now byte-exact OEM text (resolved from the literal pools + rodata via the audit auto-names). Notable: the firmware's debug labels are frequently functionally shifted/stale — e.g. cmd0x5566(horn select) logs"CMD_BLE_STATE_SOFT_RESET",0x55A2/0x55A3(testmode) log"CMD_BLE_TOP/BOTTOM_HALF_OF_DISPLAY",0x5521/0x5523swap LOCK/ALARM, and0x123(BLE-chip reset GPIO) logs"CMD_BLE_MUTE"/"Mute"/"Unmute"— all reproduced verbatim. (Themaybe_set_pending_request/display_request_setdescriptor addresses are now materialized as real C data indisplay_requests.c— see the display.c log entry.) Maps:docs/ble-commands.md, UUID schemedocs/ble-uuids.md, statesdocs/state-machine.md. -
modem.c— the u-blox SARA-G350 cellular modem driver (the bike's phone-home / anti-theft tracking path). 16 functions sourced: the AT engine (modem_at_exec0x0802F9BC,modem_at_response_match0x0802F3A0), the 10 per-state sub-state-machines (modem_step_poweron/poweroff/sms_init/sms_read/sms_write/ctx_activate/ctx_deactivate/ping_send/message_send/location_send,0x0802FDC0..0x08030620),modem_sm_state_name(0x08033070),modem_registration_get(0x0803CDE0),modem_info_ready(0x0802AAE8), andsim_iccid_check(0x0802E328) — the SIM lock that rejects any SIM whose ICCID ≠8931440400(VanMoof Vodafone-NL). Outer orchestratormodem_sim_state_machine(0x0803D284) named + documented. Data labelled:g_szIccidVanmoofPrefix(0x080507E0),g_szHostBikecomm/Ublox1/M2m,g_szAtUhttpcUpload/Bikemessage,g_pModemAtScript(0x08043EDC). Full subsystem writeup →docs/modem.md. -
console.c—volume_low_set/volume_medium_set/volume_high_set(0x080424A4/0x080423B8/0x080422CC, 236 B each). Three near-identical command handlers (vollow/volmid/volhigh); differ only by which byte in the session context they write —ctx_sub->volume_low/medium/highat+0x105/+0x106/+0x107. (Correction: the low/medium/high→byte binding is from the console command table @0x0804F5C4; the"Volume Low/Medium/High"dump labels alone are off by one, which is why this was previously mis-mapped to+0x104/5/6.+0x104is the audio-config block start, not a volume.) With no arg the handler echoes the current value; with an arg in[0, 64]("Volume 0..64"range string) it sets the byte, drives the audio amp (PE2 = enable, PD5 = mute, written viaHAL_GPIO_WritePinrecognised as the CubeF4 BSRR helper atFUN_08026AC6), and runs an audio-engine apply throughFUN_08031728(ctx->cfg[0..3]). Parsed0powers the amp down and prints"Audio off". The OEM also snapshotsctx_sub[0x104..0x1C4](192 B) onto the stack just before the apply call — the snapshot isn't read back in any decoded path, but it's preserved verbatim in our C in case some not-yet-decoded callee inspects the buffer. The C source factors the shared body into a staticvolume_set_common(input, target)helper, so the two public entry points are each 20 B trampolines; combined size is 256 B vs OEM's 472 B (saving 216 B on duplication). Behaviour-equivalent, not byte-equivalent. -
console.c—console_start_motor_update(0x08042590, 28 B). Logs"Start motor update.."and callsFUN_080313E4(4)(likely a subsystem-mode request — "4" is presumably the motor-update state). Single-line stub, byte-shape equivalent. -
console.c—console_soc_set(0x080425AC, 72 B). Bound to the consolegearcommand (help "set gear") — a firmware relabel quirk: it parses an integer withstrtol(s, NULL, 10), writes it toctx_sub->set_soc(+0x3D4), prints"Set SOC %d", and callsannounce_mark(2)to broadcast the change. No current-value echo when the argument is missing. (There is nosoccommand in the table;console_soc_setkeeps its behaviour-accurate name.) -
console.c—login_handler. The ES3 debug-console login callback (entry0x080425F4, 166 B). Reads a NUL-terminated input line and matches it first againstg_app_state.ctx_sub->user_password(the user-configurable service password at SRAM offset+0x398), then against the hard-coded fallback password baked into rodata at0x080547EC— the 40-character string"vEVjGF!paYsM2EBV8SoDT8*T0eB&#T6xevaoxCaO". The fallback works unconditionally: standardstrcmp(FUN_08021428, recognised as the canonical glibc/newlib optimised byte-then-aligned-word comparator) never reports a match between a non-empty input and an emptyuser_password, so when the user hasn't set their own password the first compare always falls through to the fallback compare. Theuser_password[0] != '\0'guard on the OEM-side after the first compare is therefore defensive dead-code in practice. On match the handler clearsfail_count, setslogged_in = 1, and prints"\r\nWelcome to ES3\r\n". On mismatch it prints"Error login\r\n", incrementsfail_count, and on the 5th consecutive miss arms a 5-second lockout via the Muco scheduler (scheduler_alloc+scheduler_start(slot, 0x1388, NULL)); any input typed during the lockout window callsscheduler_startagain on the held slot, re-arming the 5-second cooldown (anti-brute- force). The shape compiles to 292 B with-Os(GCC savesr4-r8,lrwhere OEM savedr3-r5,lr— same logic, fewer hand-optimised register reuses). Companion handlers at0x080423B8/0x080424A4/0x08042590(set-user-password, set-admin-password, set-baud) share the same line-read flow and are not yet decoded. -
systick.c—systick_tick. Identical shape to mainboot'ssystick_tick: incrementg_systick_counter(uint32) byg_systick_step(uint8) on every SysTick interrupt. The step byte lives at SRAM0x20000014— same address as in mainboot, since both wares' Muco runtime starts.dataat that offset. The counter, however, lives at SRAM0x20009704in mainware (vs0x2000083Cin mainboot) — the same Muco library linked into a bigger image gets a different.bssplacement. The instruction stream is byte-shape identical to mainboot (load counter-ptr, load counter, load step-ptr, ldrb step, add, str, bx lr); only the two literal-pool entries differ. The mainware SysTick_Handler at0x0803ca14is the Muco wrapper:bl scheduler_tick; bl systick_tick, withscheduler_tickat0x080306d8— a 48-slot version of mainboot's 16-slot scheduler (table at SRAM0x200004C0, with the bitmap + callbacks + counters scaled up). BothSysTick_Handlerandscheduler_tickare named but not yet sourced; the scheduler's larger table size means it can't share a.cfile with mainboot's. -
console.c—console_next_token(0x08040A5C, 66 B). The line tokenizer every argument-taking command handler calls first. Skips the current token (delimiters space /./:, terminator\0), then the run of delimiters, leaving*ppat the next token; returns 1 if one exists. Previously anexternstub inconsole.c; now sourced there, removing the stub. -
console.c—console_region_set(0x080421CC). Theregioncommand — the bike's speed/region mode (0=EU, 1=US, 2=JP, 3=OFFROAD; OFFROAD lifts the cap). See "Region / speed mode" above. Addedregion(+0x109) andregion_lock(+0x144) tosession_ctx. -
scheduler.c— the Muco 48-slot one-shot scheduler:scheduler_tick(0x080306D8),scheduler_alloc(0x0803073C),scheduler_release(0x080307A8),scheduler_start(0x08030800),scheduler_slot_is_idle(0x08030838),scheduler_init(0x080306C0, clears both bitmaps at startup). The slot table at SRAM0x200004C0(0x190 B) holds two 6-byte bitmaps —allocated(+0x00, set byalloc, cleared byrelease) andarmed(+0x08, set bystart, cleared byrelease, scanned bytick) — pluscallbacks[48](+0x10) andcounters[48](+0xD0). Allocation and arming are independent steps.scheduler_tickdecrements each armed slot's counter (floored at 0) and fires its callback once, when the counter lands on 1 (so a slot armed with N ticks fires after N−1 SysTick periods;login_handler's lockout uses a NULL callback and pollsscheduler_slot_is_idle). On a full tablescheduler_allocdoes not return0xFA— it callsmuco_assert_fail("src/time.c", 127)and never returns; a successful return is therefore always a valid id. (Corrects the earlier single-bitmap / "returns 0xFA" note.) -
exceptions.c— the nine Cortex-M4 system-exception handlers plus the HardFault frame dumper (0x0803C974..0x0803CA1Fon 20-byte boundaries;fault_dumpat0x0803CB6C). Built from ST's CubeF4 startup template, so each exception has its own handler.NMI/SVC/DebugMon/PendSVlog their name viag_log_funcand return;MemManage/BusFault/UsageFaultlog and spin (b .);HardFaultis a naked tail-call (tst lr,#4; ite eq; mrs r0,msp/psp; b.w fault_dump) that hands the active exception frame tofault_dump, which prints the 8 stacked registers (R0-R3,R12,LR,PC,xPSR) and the SCB fault registers (MMFAR/BFAR/CFSR/HFSR/DFSR/AFSRat0xE000ED34/38/28/2C/30/3C) then spins.SysTick_Handleris the Muco tick:scheduler_tick()thensystick_tick(). The naked HardFault and thetst lr,#4SP-select assemble byte-for-byte to the OEM; the log handlers are behaviour-equivalent. -
panic.c—muco_assert_fail(0x0803DAC4). The Muco runtime fatal handler called from ~10 sites (incl.scheduler_alloc):g_log_func("FATAL error File [%s] line [%d]\r\n", file, line)then ab .spin (IWDG reboots). Marked no-return; the format string lives at rodata0x08053300.g_log_funcis the SRAM0x20009D98logger slot shared with the console and every exception handler. -
memcpy(0x08020EC0) recognised as newlib's optimisedmemcpy(unaligned head, 16-word unrolled body, word/half/byte tail; returnsdst). Marked vendor-stock — supplies the snapshot copy involume_set_common;console.calready callsmemcpyvia<string.h>. -
app.c— two application-core leaf primitives shared by the console and the super-loop.update_mode_request(0x080313E4) sets the subsystem firmware-update mode byte (SRAM0x20000076) only when idle (current value 2), so an in-progress update can't be preempted; callers are the loop state machine, the motor handler, and themotorupdateconsole command (mode 4).announce_mark(0x0802F1C0) sets a per-channel broadcast dirty-flag (SRAM0x2000028D/0x2000028Efor channel 0/1); any other channel is a no-op. Finding:console_soc_setcallsannounce_mark(2)— a no-op — yet the SOC override still applies because the super-loop consumesctx->set_soc(FUN_08043C74(ctx+0x3D4)) every iteration. The prior "announces via FUN_0802F1C0(2)" note was misleading; corrected. -
systick.c— the Muco timebase readers, siblings ofsystick_tick.systick_now(0x080232F8) returnsg_systick_counter(ms since boot, 1 ms/tick) —systick_nowis byte-identical to the OEM.systick_delay(0x08023304) busy-waitsticksperiods, rounding up byg_systick_step(DAT confirmed0x20000014) so the caller gets ≥tickswhole periods;0xFFFFFFFFblocks forever. These are the timebase the whole app polls against (the charger SM,systick_delay(10/100)pulses, the per-subsystem throttles viaFUN_08029B24's 100-tick sampler, etc.). -
util.c— pure packed-BCD <-> binary byte converters:bcd_to_bin(0x0802311C,(bcd&0xF)+(bcd>>4)*10) andbin_to_bcd(0x08022F2E, count-tens loop). RTC/time helpers; both transcribed from the OEM arithmetic. Found via an image-wide small-leaf sweep — most small leaves on this image are opaque state accessors, so these (and the two vendor-stock recognitions below) were curated rather than bulk-banking meaningless accessors. -
system_stm32f413.c—SystemInit(0x08043AA4), the stock CubeF4 template: enable the FPU (CPACR |= 0xF00000), reset RCC (HSION, clear HSEON/CSSON/PLLON viaCR &= 0xFEF6FFFF,PLLCFGR = 0x24003010, clear HSEBYP,CIR = 0), setVTOR = 0x08000000. Compiles to the OEM's exact operation sequence (behaviour-equivalent; the two-bicsplit matches). -
startup_stm32f413.S— the keystone. The 512-B VanMoof envelope (byte-perfect bar the post-build CRC/length), the 128-entry vector table (system slots → our handlers, IRQs →Default_Handler), andReset_Handler(0x08043E54, asm: set SP, copy.data, zero.bss, paint[_ebss,_estack)with0x0000000E, thenSystemInit→__libc_init_array→main). Plus weakmain/__libc_init_arrayplaceholders so the partial tree links. -
Vendor-stock recognised this sweep:
__floatsidf(0x08020980, libgcc soft-float int→double — doubles go through libgcc since the FPU is single-precision) and__getreent(0x08020DEC, newlib_impure_ptrgetter,*0x2000011C).
main (0x0803DEA8) + the boot chain are now SOURCED → src/main.c (full
map in docs/boot.md), adversarially verified behaviour-equivalent against the
disassembly. main is defined __attribute__((weak)) so the strong startup
spin-stub stays the link entry until the ~70-callee closure is sourced — the
real loop compiles + warning-checks but gc-sections out (build stays clean,
text 1996); flip to strong + drop the stub to root the graph. The ~74 direct
callees were named this pass. The shape:
Entry / one-time init. First instruction writes SCB->VTOR = 0x08020200
— mainware re-points the vector table to its own table, overriding the
stock 0x08000000 that SystemInit wrote (resolves the VTOR open question).
Then FUN_080232AC(), cpsie i (enable IRQs), and a boot-marker branch:
if SRAM 0x20000000 == 0x55AA55CF → boot_init_warm (0x0803DADC), else
boot_init_cold (0x0803DDE0) followed by the full ~40-call init sequence
(subsystem registrations FUN_080332xx/FUN_0803c2xx, GPIO/peripheral
bring-up, HAL_GPIO_WritePin(GPIOD, PD7, 1), etc.). The marker lives in the
retained low-RAM region below .data (see hardware.md), so it survives a
warm reset and lets mainware skip cold init.
Super-loop (do { … } while (true)), per iteration, in order:
- tick/poll (
FUN_080314D8), per-subsystem serviceFUN_080371E8(ctx); - if
ctx[0x34D]==0→FUN_0803D284(); ifctx[0x402]==1→ 3×FUN_08037B64(a 3-channel update, one per subsystem index 0/1/2); - SSP/messaging poll:
FUN_0803CC6C,FUN_0802945C,FUN_0803F338,FUN_08036B98; BLE-message checkFUN_0803F8FC→ event0x800000;FUN_0803F6B4→ logs" ERROR SSP BLE msg removed"; - charge state
FUN_08030FF4()==3branch → motor-message confirm (FUN_0803A278), logs" ERROR SSP MOTOR msg not confirmed"; - display
FUN_0803BB40()→ logs"ERR Display"; - battery/voltage update:
ctx[0x3B0]=FUN_08029B24();ctx[0x3C0]=FUN_08038F78()orctx[0x3C4]depending onctx[0x3C6]; - lock/kickstand logic (
ctx[0x313]/[0x314]/[0x3B8]&0x800000,FUN_08032980) →FUN_08029774(flag); - tail:
FUN_0802E800(ctx),FUN_08038A90(ctx),FUN_08043C74(ctx+0x3D4)(consumes the SOC override).
The event log strings are reached via g_log_func (0x20009D98). The
super-loop never sleeps — it's a busy poll with the scheduler/SysTick
driving timed work underneath. The application context is the global struct
at SRAM 0x200083A8 (same object the console reaches through
g_app_state.ctx_sub; the +0x3D4/audio-block offsets line up). The next
sub-targets are its leaf callees (e.g. the SSP poll helpers and FUN_08043C74).
subsystem_update_sm (0x08031900, called from the super-loop with the
context) is mainware's whole-bike firmware-update engine — it pushes new
firmware to every other MCU on the bike over the inter-module bus. A ~22-state
machine (state byte at *DAT_08031bbc):
- Manifest + target identification. It walks a list of pending firmware
files (
countatDAT_08031edc[1]) and identifies each target by matching the 2-char prefix of its filename (FUN_08020E60(slot, id, 2)) against rodata at0x08050FF4+:"ma"→mainware,"mo"→motorware,"sh"→shifterware,"ba"→batteryware,"ble"→bleware. The matched type selects the per-subsystem mode written toctx[0x32C + idx](1..5) and a target flash base / size cap (0x40000/0x20000/0x10000per type). - Transfer. Modbus/SSP writes via
FUN_0803F9CC(reg, len, buf, …)— control registers0x104(update command),0x110(mode/abort),0x11B/0x11C(finalize) — interleaved with block read/write (FUN_0803CF30/FUN_0803CF94/FUN_0803CFD8) andscheduler-driven timeouts. Progress is logged as"Process: %s %d bytes on 0x%08X";"Shifter power on"powers a target up first. - Entry/exit. State
0xBconsumesupdate_mode_request(4)(the value themotorupdateconsole command sets — seeapp.c) and only proceeds when a subsystem reports update-ready (ctx[0x402]==1 || ctx[0x400]==1). State0x15= abort/error (callsFUN_08029CA0(code)),0x16= done/cleanup; the final stages can trigger a self-reset of mainware (0x16/case 2: DSB + AIRCR write + spin).
The full pipeline lives in this one state machine (the strings all xref back into it):
- Receive (BLE).
"Start BLE update"→ packets arrive viable_ssp_dispatch(0x0803F8FC) from the bleware MCU over the bus. Each packet header is validated ("Invalid packet header","No packet found"), with CRC ("ERR BLE-CRC") and per-packet timeouts (packet_tmr,between_pack_tmr). - Stage. Firmware is written to shadow flash (
"Erasing shadow flash %d Kb","Flash write error %d") and the update table / manifest is built ("Invalid update table") — the per-file listsubsystem_update_smthen walks (matching*ware.binprefixes). - Flash each subsystem over the bus: Shifter (
"Send Shifter erase","Shifter CRC error","crc check ok"), Battery/BMS ("Send battery erase","BMS CRC error","Wait for BMS internal update!"), Motor ("Update F2806 CPU", the TI C2000), BLE. Each: erase → write blocks → CRC-check → done, with"No need for X update"skips and vbat guards ("Shifter update no vbat (%d)"). - Mainware self-update can't flash itself live:
"Update Main by reboot, wait for BMS shutdown"— it reboots so the loader (muco-boot) applies the staged image."OAD_UPDATE"is the bike state during all this.
This ties the console, the loop, and the bus together: motorupdate →
update_mode_request(4) → subsystem_update_sm flashes the named MCU. It's
deep (dozens of callees) so it stays mapped-not-sourced, but it's the single
most important behaviour in the firmware for the "understand, then modify" goal.
Correction: the uBlox cellular modem is the anti-theft tracking /
telemetry uplink, not the OTA firmware-download channel. OTA firmware
arrives over BLE (phone app → bleware on the CC2642 → inter-module bus →
mainware → subsystem_update_sm). The modem path's request builders are thin
snprintf wrappers around the two backends:
bikecomm_request_build(0x0802F8A0) →"bikecomm.vanmoof.com"(0x08050890) — the bike↔cloud message channel (telemetry, location, commands).ublox_request_build(0x0802F940) →"ublox1.vanmoof.com"(0x080508C0) — the modem HTTP endpoint. Both callsnprintf(0x080212B0, newlib vendor-stock); the modem driver (AT+UHTTPC) transports them. The SIM is checked against an expected ICCID ("Wrong iccid, %s",FUN_0802E328). These belong to the tracking subsystem.
OTA download (BLE) — mapped. The firmware bytes come from the phone app over
BLE; ble_ssp_dispatch (0x0803F8FC, called from main's super-loop) is the
BLE/SSP message receiver — it pulls messages bridged from the bleware MCU
across the inter-module bus (logs "ERR BLE SSP packet not in queue"). Firmware
packets flow through it into subsystem_update_sm, which does the receive →
stage → flash (see the dedicated section above and below).
The bike's fault state lives in two words of the application context:
ctx[0x3BC]— the 32-bit error/fault-flags word. Each set bit is a distinct fault. The "Error Flags" report (insidestatus_process, around0x0802ACE2) rotates the word bit-by-bit and logs each set bit's index viag_log_func("Error Flags: %d", bit), or"Error Flags: None"when clear.ctx[0x3B8]— a status word. Its0x800000bit gates the kickstand/lock decision inmain's super-loop.- Diag self-test (around
0x0802D540) loads both as a 64-bit pair (ldrd [ctx,#0x3B8]):(status | errors) == 0→"Diag ok", else"Diag fail"plus it raises an error state (0x22) and an action.
These are emitted by status_process (0x0802AAF8, ~14 KB) — the large
per-loop bike-status/event processor main calls each iteration (named,
not sourced: the function is too big for the decompiler and likely merges
several logical handlers). Struct note / TODO: ctx[0x3B8..0x3C7]
(status/error/battery fields) fall inside the range I tentatively modelled as
session_ctx.user_password[0x3C] (+0x398..+0x3D3) — so the password buffer is
actually shorter (≤ 0x20, +0x398..+0x3B7) and the status/error words follow.
app_state.h is left unchanged pending a confident re-model (the fields also
overlap union-style: ctx[0x3C4] u32 vs ctx[0x3C6] u16).
console_region_set (0x080421CC, sourced) sets ctx_sub->region (+0x109):
0=REGION_EU, 1=REGION_US, 2=REGION_JP, 3=REGION_OFFROAD. EU/US/JP are the
regulated speed caps; OFFROAD removes the cap. A change is accepted only when
the lock state ctx_sub->region_lock (+0x144) is < 3 (states: 1=off-road
disabled, 2=locked, else unlocked) — that byte is set elsewhere (likely the
BLE/app or cloud lock path, not this command). Applying a region re-runs the
config-apply FUN_08031728, which pushes the ctx_sub[0xF4..0x103] block to the
drive subsystem — i.e. the speed cap is enforced by the motor controller, and
this command is what reconfigures it. alarm_state_name (0x08032DF0, was
lock_state_name) is the state→name table (62 entries; 0–3 = ALARM_*) used
for logging alarm/security states.
gpio_init(0x080314E8) — board GPIO bring-up: enables the GPIO port clocks (RCC AHB1ENR bits), sets initial output levels viaHAL_GPIO_WritePinon ports D/E/etc. (masks0x102C,0xC32F,0xBCF0,0x9000…), then configures each pin's mode/speed/pull viaFUN_080267D0. The authoritative pin map for mainware — sourcing it (andFUN_080267D0) will populatehardware.md's GPIO table.config_crc_check(0x0802968C) — validates an 8-byte config block with a CRC-16 (FUN_0803C2C8(p, 8, 0xFFFF)) against the stored value at+8; on mismatch it resets the block (FUN_0802957C) and re-persists it. Config integrity guard, run during init.
The phone app's BLE link is bridged by the bleware MCU onto the inter-module bus; mainware receives it as the "SSP" stream. The transport is now sourced:
slip_rx_packet(0x0803F5A4,ssp.c) — a SLIP de-framer (RFC-1055:0xC0END,0xDBESC,0xDC/0xDDescaped END/ESC) with a CRC-16 trailer (crc16,0x0803C2C8, the Modbus-bus CRC). Bytes are popped one at a time from the RX ring byssp_rx_byte(0x08036528). Returns 0 = complete valid frame, 2 = CRC/framing error ("ERR BLE-CRC","BLE-SEQ"), 1 = receiving. The de-framer state lives at the SSP context0x20008A40 + 0x600.ble_ssp_dispatch(0x0803F8FC,ssp.c) — pulls one frame and dispatches by message type (byte [1]):0x05= command (ble_command),0x06= prepare/length,0x07= data (command id [3..4] + payload [7..] →ble_cmd_dispatch). Sends a{1,5,id}ack. Called every super-loop pass.
ble_cmd_dispatch (0x08033970, named — a ~2.5 KB switch, mapped not
sourced) is the BLE app-command surface: a big switch on the 16-bit command
id. Highlights decoded from it:
0x5521lock/unlock,0x5523power-state set (off/standby/on…),0x5562unlock-with-state-checks.0x5534/0x5535/0x553C= region/speed over BLE — the app equivalent of the consoleregioncommand: setsctx[0x109]/ctx[0x3C9]and the speed capctx[0x354](region 1→180, 2→120, 3→60, 4→30 in its unit;0xFF→ OFFROAD). So the speed limit is settable from the app, gated by the samectx[0x144]lock.0x5503SOC,0x5537assist-curve,0x5566/0x5574power mode,0x5582LEDs,0x5564alarm,0x5572set module key (ctx[0xF4..0xFC]),0x55C1log control. Most handlers end by echoing back viaFUN_0803F9CC(id, …)(the bus reply) and re-applying the config block (FUN_08031728).
The OTA firmware data path also enters here (type 0x07 / the update command
ids 0x104/0x110/0x11B); the staging into shadow flash is now decoded — see
below.
ble_cmd_dispatch is the production control surface — the ~40-case switch on
the 16-bit command id that turns phone-app GATT writes into bike actions (the
parallel to the debug console). It's named + documented (deep switch, not
sourced); the full command→action table is in docs/ble-commands.md.
Highlights: 0xFA power on/off; 0x5521/0x5523/0x5562 lock / unlock / alarm
arm; 0x5535/0x553c region + speed mode (+ region-lock); 0x5534 motor power
level; 0x5503/0x5572 backup code; 0x5533 units; 0x5581 light mode; 0x5582
LEDs; 0x5524 alarm; 0x5536 shift gear; 0x5537 speed moments; 0x5538
transmission; 0x5564 wheel size; 0x5571 play sound; 0x55C1 log-to-app;
0x11A provisions the BLE MAC + serial; 0x55A2/0x55A3 arm test/CRC modes. Each
write mutates ctx field(s), config_persist_dual_bank / save_state_record_to_eeprom,
gives UI/sound feedback (channel_notify_with_status), and acks via
ssp_ble_enqueue_tx_packet(cmd, …). The read twin (phone polling state) is
ble_read_request_dispatch (0x08034D20). The ctx offsets line up with the
show dump + console.md (region +0x109, backup code +0x100, units +0x10A,
light +0x10C, wheel +0x10B, distance +0x31C, alarm +0x317, power level +0x3C9).
The read/telemetry twin ble_read_request_dispatch (0x08034D20) is also
mapped (the ## Read / telemetry surface table in docs/ble-commands.md) — what
the app polls to render bike state (battery summary 0x5541, error flags 0x5563,
buttons 0x5568, version strings 0x5549–0x5550, plus the read-back of every config
write). The lock/alarm reads collapse the fine state byte into coarse app enums
via ble_lock_state_get/ble_unlock_state_get/bike_status_coarse_get/
bike_state_is_standby (+ bike_is_locked, the lock-pin PC8 + flag check).
The bike lock/alarm state machine is documented in docs/state-machine.md:
the state byte @ SRAM 0x2000002D holds the alarm_state_name codes (0..0x3D,
grouped into boot/ride, standby/lock, unlock, PIN, alarm/tracking, shipping,
power/sleep, charging, OTA, diag); maybe_set_state_if_unlocked is the write gate
(refuses to overwrite shipping states 6/7); and the BLE/console commands are the
transition inputs (e.g. 0x5523→alarm/lock, 0x5521→lock/unlock, 0xFA→power,
setoad→OAD 0x19, shipping→0x07).
Once a firmware PACK is received and validated, subsystem_update_sm erases
the destination sectors and programs the image. Decoded:
pack_validate(0x0803CFD8, sourced →flash.c) — the PACK validator. The PACK begins with the VanMoof magic0xAA55AA55(DAT_0803D068, the same magic as the image envelope); rejects length ≥0x40000(256 KB). It resets the CRC unit, then runs the hardware CRC-32 (crc32_hw_feed0x0802320E, STM32 CRC peripheral) over the 10-word header (with the stored-CRC + length words masked to0xFFFFFFFF) immediately followed by the body —(len − 0x28)/4words, one continuous CRC — and compares it to the stored CRCpack[2]. Returns 0 = valid, 1 = CRC mismatch, 2 = bad magic / too big.flash_erase(0x0803CF30, sourced →flash.c) — erases every flash sector overlapping[addr, addr+len):flash_addr_to_sector(0x0803CE14, sourced) bounds the range,HAL_FLASHEx_Erase(0x080235B4, CubeF4 HAL with a sectorEraseInit) clears each, kicking the watchdog between sectors. (Behind"Erasing shadow flash %d Kb".)flash_write(0x0803CF94, sourced →flash.c) — masks IRQs, waits for any in-flight flash op (FLASH_WaitForLastOperation0x08027B80), clearsFLASH_SR(0x40023C0C←0xF3), then programs the image one 32-bit word at a time viaHAL_FLASH_Program(0x08027BE0, the width-tagged HAL — op 2 = word; op 0/1/3 = byte/half/dword). Both compile faithfully;flash_writeeven reproduces the OEM quirk of leaving IRQs masked on a program error.
So the OTA write path end-to-end: BLE SLIP frame → ble_ssp_dispatch →
pack_validate (magic + CRC-32) → flash_erase (sectors) → flash_write
(words) → subsystem_update_sm pushes/verifies each subsystem.
A fan-out workflow analysed 19 functions (10 returned, adversarially verified); 8 were sourced and the rest named. Highlights, including two corrections to earlier guesses:
- BLE/SSP TX queue.
ssp_ble_enqueue_tx_packet(0x0803F9CC, sourced →ssp.c) is the TX twin ofble_ssp_dispatch(RX): a 128-slot queue of 12-byte descriptors{flags, type=5, seq, cmd16, len16, payload_ptr}with a rolling unique sequence id. This confirms0x20008A40is one unified SSP context — the 128×12 = 0x600-byte TX queue occupies+0x000..+0x5FF, the SLIP de-framer state is at+0x600, the ack frame at+0x710(the_pad0in the earlierssp.cmodel was the TX queue). Correction:0x0803F9CCis the BLE SSP enqueue (assert file"src/ssp_ble.c"), not a Modbus subsystem-bus TX as previously guessed. - Bike-state persistence to EEPROM.
save_state_record_to_eeprom(0x0803E2CC, named) CRCs the 56-byte state recordctx[0x310..0x348]through the hardware CRC unit, appends the 4-byte CRC, and writes the 60-byte record to an I2C AT24C EEPROM (slave0xA0), double-stored at EEPROM offsets 0 and 0x40. Correction: earlier docs called0x0803E2CCa "bike-state broadcast/apply"; it is the persistence path — so theble_cmd_dispatch/subsystem_update_smcalls to it are "save the state record to EEPROM" after a change. Not sourced: it relies on a stack-aliasing ABI (only 4 reg args, but reads the caller-spilled 56-byte record), which C can't reproduce faithfully. ble_read_request_dispatch(0x08034D20, named, ~3.9 KB) — the GATT/SSP characteristic-READ dispatcher (the read twin ofble_cmd_dispatch's writes): ~45 cases0x5503..0x55C1(+ motor-option0x14/0x19) gather a ctx field and reply viassp_ble_enqueue_tx_packet; unknown ids log"Unhandeled SSP request %04X". Several values use the0xCCCCCCCD/>>35divide-by-10 scaling.testmode_command_dispatch(0x08029CA0, named) — the "SH Set testmode" command (selector 0–12): selector 1 publishes a version/serial/HW telemetry batch over BLE/SSP svc0x554A..0x5550; selectors 2–0xC arm a 64-bit action bitmask atctx+0x3B8/0x3BC(note:0x3BCis the error-flags word) and post event 0x18. Errors are only logged, never raised.- Leaves sourced:
crc16+crc16_modbus_update(crc.c, Modbus poly0xA001— completes the bus CRC);flash_program_word+flash_unlock_and_clear_status(flash.c, the FLASH HAL behind the staging);ringbuf_get_byte(util.c, the generic FIFO dequeue behindssp_rx_byteand ~15 others);amp_volume_brownout_apply(audio.c, resolving the lastconsole.cFUN_080391B8stub — supply-clamped volume + amp GPIO + bus TX);maybe_set_state_if_unlocked(app.c, FSM state byte setter, states 6/7 locked).
A second fan-out workflow analysed 12 functions (all returned, verified); 9 sourced, 3 named. Highlights:
- BLE/SSP transport now complete end-to-end (
ssp.c). The four remaining handlers are sourced:ble_data_packet/ble_prepare_packet(the type-0x07 / 0x06 thunks intoble_cmd_dispatch/ble_read_request_dispatch),ble_command(type-0x05 = an ACK that finds the queued TX packet whose sequence id matches, clears the slot, and runs its completion callback), andslip_send_frame(the SLIP TX: CRC-16 +0xC0framing + escaping — the counterpart ofslip_rx_packet, and whatble_ssp_dispatchuses to send its ack). Full path: RX de-frame → dispatch by type → TX ack/notify. - FSM state byte get/set pair.
maybe_get_bike_state(0x08029BA0) reads the byte at SRAM0x2000002D;maybe_set_state_if_unlocked(0x08029B88, batch 1) writes it unless it's 6/7. So0x2000002Dis the bike's lock/power state code (the valuealarm_state_namestringifies). config_persist_dual_bank(0x08031728, named — correction of the earlier "config-apply to subsystem"): after a volume/region change it persists the config to two redundant internal-flash banks (primary0x08008000, backup0x0800C000) via a flash erase+write+CRC-verify worker, OR-ing the two status codes. Named only (stack-aliasing ABI likesave_state_record_to_eeprom).- Other leaves sourced:
log_print_timestamp_prefix("%02d/%02d:%02d:%02d "date/time prefix →log.c),supply_voltage_read(raw ADC → fixed-point → 10-sample moving average →sensor.c),maybe_set_pending_requestandchannel_notify_with_status(→app.c). Named-only:enter_mode3_with_periodic_task(a mode-3 + periodic-task state transition) andflash_addr_to_sector.
A third fan-out analysed 14 functions (13 returned, 10 leaf-source); the focus
was closing out extern FUN_* references the recent modules left dangling.
5 sourced, 2 recognised as vendor-stock, 6 named+documented (with plate
comments in Ghidra). Highlights:
- The image's allocator is plain newlib.
FUN_08020E40=malloc,FUN_08020E50=free(both tail into_malloc_r/_free_ron the_impure_ptrreentrancy struct at0x2000011C). So inssp.cthe TX path is now honest:ssp_ble_enqueue_tx_packetmallocs the payload copy andble_command(the type-0x05 ACK)frees it — there was no "completion callback", just afree(batch-2 wording corrected). uart_send_byte(0x080364F0, →uart.c, new module). The serial TX primitiveslip_send_framecalls per byte: masks the device TX interrupt (clears bit 7 ofdev+0xC, DSB/ISB), pushes the byte into the TX ring viaringbuf_push_byte(0x08031874), re-enables the interrupt. ABI quirk preserved: it returns no value of its own —r0survives from the ring push, so the function implicitly returns the push status (1 queued / 0 full), which is exactly whatslip_send_framemaps to its TX-full error. Verified by cross-readingslip_send_frame'sif (uart_send_byte(0xC0) == 0)tail.moving_avg10_push(0x08032AB0, →sensor.c) — the smoothing stage ofsupply_voltage_read. Shares the ADC context struct at0x20000914: write cursor (byte) at+0x00, tenu16samples at+0x04, then the ADC status byte+0x22and raw sample+0x2Aused bysupply_voltage_read. (The adversarial verifier caught a 1-vs-3 padding-byte layout error — samples land at+0x04, not+0x02.) Division by 10 via the0xCCCCCCCDreciprocal idiom.channel_resolve_status(0x0802A2B0, named→sourced inapp.c): the helper behindchannel_notify_with_status. Resolves a channel id against three priority bitmasks in the app context (*0x20000944+0xF4/0xF8/0xFC), returning 1..3 by priority or 0.state_flag_get/state_flag_set(0x08036B8C/0x08036B80, →app.c): the byte at SRAM0x20000083thatlog_print_timestamp_prefixbrackets (save/zero/restore) — confirmed a plain state/mode flag, not an IRQ mask.- Named+documented (hint corrections in bold were caught by the agents):
flash_config_bank_write(0x080316D0, the single-bank worker underconfig_persist_dual_bank: erase → CRC-in-record → write0xD0→ CRC-verify; stack-aliasing ABI, name-only);mainware_boot_init_sequence(0x0803FC94— not a flash routine: the ~1.5 KB top-level startup orchestrator — version print, GPIO/flash bring-up, per-subsystem init with a fault-retry/recovery loop, state-record validation, dual-bank config persist);sms_info_tracking_state_machine(0x0803CC6C— not an SSP service: the modem SMS info-tracking scheduler, part of the anti-theft path);modbus_bat_service_step(0x0803F338— the battery (BMS) Modbus link service step, distinct from BLE/SSP; retry/flush on the0x80000region tag);maybe_enqueue_tx_message(0x0803A1C4— not a per-characteristic GATT send: a generic enqueue into a second 16×24-byte TX table at0x20007E14, gated on link-connected);rtc_fill_time_fields(0x080380A4, STM32 HALGetTime/GetDate→ buf[0]=hr [1]=min [2]=sec [0x16]=day).
A fourth fan-out analysed 14 functions (all returned, 13 leaf/thunk); the goal was to complete the flash/ssp/crc/util modules and source the cloud builders. 9 sourced, 4 recognised as stock CubeF4 HAL, 1 thunk left provisional.
flash.cis now self-contained.flash_addr_to_sector(the F413 1 MB sector map, unsigned-underflow range tests) is sourced. The three HAL leaves it/flash_writelean on are recognised as stock CubeF4 and marked vendor-stock:HAL_FLASH_Program(0x08027BE0),HAL_FLASH_Unlock(0x08027B14), and — correcting the earlierlock_acquiremisnomer —FLASH_WaitForLastOperation(0x08027B80). The latter fix makesflash_writeread correctly: it waits for the prior flash op (timeout0xFFFF) before programming, it does not "acquire a lock".ssp.cRX/TX primitives sourced.ssp_rx_byte(atomic RX-ring pop, RX IRQ masked via a double pointer hop*(*0x20009864)+0xC) — the agent's first pass dropped one deref; the verifier caught it. Same implicit-return ABI quirk asuart_send_byte(returns the ring-get status throughr0), which is whatslip_rx_packet's== 0test reads.ssp_ble_seq_id_in_usescans all 128 TX slots' seq byte.util.cring is complete —ringbuf_push_byteis the enqueue twin ofringbuf_get_byte(advancehead, wrap atcap, bumpcount).crc.c—crc32_hw_feed(0x0802320E) streams words into the STM32 CRC peripheral via a driver handle at SRAM0x20009D90;HAL_CRC_Accumulate(0x08023234, the oldcrc_stream_words) is its stock-HAL sibling → vendor. So0x20009D90is the CRC HAL handle, refining the batch-3 "config_crc_state" note forflash_config_bank_write.net.c(new module) —bikecomm_request_build/ublox_request_build:snprintfthe cloud/modem HTTP request strings. Resolved the fixed flash strings:bikecomm.vanmoof.com(0x08050890),ublox1.vanmoof.com(0x080508C0), and the u-blox auth token"PBNjh0V46Eev8CcfS4LPJg"(0x080508D4, passed as the 3rd%s). These feed the modem AT+UHTTPC uplink.log_buffer_crc_check(0x0802968C, was mislabelledconfig_crc_check) →log.c: CRC-16 over the 8-byte circular-log-buffer header at SRAM0x20037000; on mismatch resets the buffer and writes"Log cleared because invalid CRC". Named its two helperslog_buffer_reset/log_emit_string.channel_notify_emit(0x0802A064) →app.c: the sourced body behindchannel_notify_with_status. For the four sound channel codes (5/0x1B/0x1C/ 0x1D) while the context mode byte (*0x20000944+0x310) is0x0Bit runs a sound/clocking scheduler sub-path (force-set state0x3D, alloc+arm a slot for a code-specific duration), then always: amp-volume apply (brownout-limited), log"SOUND_S%c vol %d", and enqueue a 2-byte BLE notify{code,1}as SSP command0xC8.- Provisional:
0x080314D8(watchdog_kick) is really a thunk toFUN_08026DB4(desc@0x20009728)(storedesc[0xC]to*desc[0]); the watchdog reading rests on the call-site pattern, not confirmed bytes — name kept with a caveat.
A fifth fan-out analysed 12 functions (all returned, 10 leaf/thunk). 9 sourced,
2 recognised as stock CubeF4 HAL, 1 deferred. New modules gpio.c + eeprom.c.
pack_validate(0x0803CFD8) →flash.c— the OTA image validator. PACK magic is0xAA55AA55(the verifier corrected the agent's byte-swapped0x55AA55AA), length <0x40000. It resets the STM32 CRC unit, then hardware- CRC-32s the 10-word header (with the stored-CRC + length words masked to0xFFFFFFFF) immediately followed by the body — one continuous CRC, no reset between — and compares vspack[2]. The verifier also caught an extra-deref on the CRC-handle arg (the struct base0x20009D90is passed directly, not its contents). Returns 0 ok / 1 CRC fail / 2 bad-magic-or-length.gpio.c(new) —gpio_init(0x080314E8): the mainware pin map. RCC AHB1 clock enables (E/H/C/A/B/D), initial output levels, then per-pin config via the now-recognised stockHAL_GPIO_Init(0x080267D0, vendor). Pin masks/levels folded into hardware.md.eeprom.c(new) —eeprom_write_region(0x0803E258): page-split (<=8-byte) writes to the AT24C I2C EEPROM (dev0xA0) viaHAL_I2C_Mem_Write(0x08024D2C), 5 ms + watchdog between pages; addressoffset | (page<<3).log.cbuffer ops —log_buffer_reset(0x0802957C, zero the 100 KB payload + revalidate header CRC + console "Log cleared") andlog_emit_string(0x0802963C, append with oldest-line eviction on overflow). Confirmed0x20009D98is a single function pointer (g_log_func, one deref + call) — validating the project-widelog_func_tmodel. Shared helperlog_buffer_header_crc_update(0x08029564) named.app.cmode/clocking leaves —aux_mode_byte_get/set_mode_state_byte(0x0802F0F8/0x0802E7F4, the byte0x20000068thatchannel_notify_emitreads/writes — its callees now resolved);enter_mode3_arm_show_timer(0x0802F104, wasenter_mode3_with_periodic_task— the0x20000288display-field-vs-g_announceoverlap is now understood: byte+4 = display sub-field); andclock_pulse_gpioa8_until_pc9(0x0803C8F4) — the "Clocking %d" value is a GPIO bit-bang: pulse PA8 (3 ms/3 ms) up to 200× until PC9 goes high, returning the pulse count (a hardware clock/calibration probe).- Deferred:
alarm_state_name(0x08032DF0, renamed fromlock_state_name) is astate→nametable; only cases 0–3 (ALARM_PRE_M1,ALARM_ACTIVE_M1_CNT,ALARM_ACTIVE_M2,ALARM_TRACKING_UNCONFIRMED) + defaultUNKNOWNwere resolved. Sourcing the other 58 with placeholder text would emit wrong log strings, so it stays named until the strings are read. - Also recognised vendor:
HAL_FLASHEx_Erase(0x080235B4, theflash_erasesector-erase callee) andHAL_GPIO_ReadPin(0x08026AB8).
A sixth fan-out analysed 12 functions (all returned, 10 leaf/thunk, all
verified faithful). 10 sourced, 2 vendor. Two new modules i2c.c + watchdog.c.
This batch closed out the externs the recent modules were leaning on.
log.cis self-contained — the four SRAM circular-log-buffer primitives are sourced:log_putc/log_getc(cursor advance + wrap at the0x18C00end marker; write cursor @ header+4, read cursor @ header+0),log_drain_line(pop a line, oldest-first), andlog_buffer_header_crc_update(CRC-16 over the 8-byte header).strlenis recognised newlib (vendor). So the full log path —log_emit_string→log_putc/eviction → header CRC — is now real C.watchdog_kickresolved (0x080314D8→watchdog.c): the provisional thunk is confirmed a window-watchdog refresh —wdg_reg_write_from_desc(0x08026DB4) writesdesc[3]=0x7Fto*desc[0]=WWDG_CR(0x40002C00).clock_pulse_gpioa8_until_pc9reframed as I2C3 bus recovery. Its two callees arei2c3_handle_deinit(0x0803C8E4, thunk →HAL_I2C_DeInit) andi2c3_handle_init(0x0803C660, sets Instance=I2C30x40005C00, 100 kHz) → newi2c.c. So PA8 = SCL bit-bang, PC9 = SDA sense, and the routine de-inits I2C3, clocks SCL until SDA releases, then re-inits — the classic stuck-bus recovery. The I2C3 handle0x20009B04is the same handle the EEPROM uses.enter_mode3callees resolved:scheduler_set_timer_name(0x08029B70, abx lrstub in this release build — the debug timer-name register) →scheduler.c;reset_dual_buffers_and_flags(0x0803B780, zeroes the two 0x96-byte buffers ing_request_ctx@0x20008230) →app.c.update_mode_get(0x080313D8) reads SRAM0x20000076— the same byte asg_update_mode; callers (e.g.maybe_enqueue_tx_message) treat value 2 (idle) as "link ready", so it's the getter twin ofupdate_mode_request.- Confirmed
0x20009D98is a single function pointer once more (the log primitives call it directly), and0x20037000is the 12-byte log header + ~100 KB payload to0x2004FC00.
The batch-7 workflow itself didn't land (its agents explored but never emitted
the structured result on the harder capstone targets, then the Ghidra MCP server
went unresponsive). But alarm_state_name (0x08032DF0) was resolved directly
from the OEM image by parsing its Thumb tbh jump table + literal pool +
strings — and sourced to src/states.c (62 cases + UNKNOWN; the Ghidra
rename/prototype were already done in batch 5, so no MCP was needed). Cases 0–3
match the batch-5-verified values exactly, confirming the parse.
These are the bike's top-level state codes (what maybe_get_bike_state
tracks and status_process logs), and they map the whole behavioural surface:
- alarm / anti-theft:
ALARM_PRE_M1,ALARM_ACTIVE_M1_CNT,ALARM_ACTIVE_M2,ALARM_TRACKING_UNCONFIRMED/_CONFIRMED,ALARM_BMS_REMOVED,ALARM_DELAY_ON; - shipping / power:
SET_SHIPPING/SHIPPING,BIKE_SHIPPING_ACCIDENTAL_WAKE,BIKE_SHIPPING_LIPOCHARGE,STANDBY,CPU_STOP_MODE/CPU_STOPPED,AUTOWAKEUP,LIPOCHARGE,CHARGING,LOW_SOC,TURN_ON,RIDING_MODE; - OTA (OAD):
OAD_UPDATE,OAD_FILE_TRF,OAD_RX_SOUND,OAD_FINISH,OAD_FAILED— the BLE firmware-update state sequence; - PIN / lock:
PIN_START/STUCK/1ST/2ND/3ND/CHECK/OK/SHOW_OK/NOK/NOK_SHOW,UNLOCK,EXTRA_ALREADY_UNLOCKED,UNLOCK_COUNT(_TIMEOUT),LOCK_PLAY_*,LOCK_DIM_OFF,LOCK_CLEAR,LOCK_SETUP,LOCK_PIC,LOCK_COUNT; - misc:
INIT,RESET,DIAGNOSE,DIAG_RDY,FACTORY_TEST,SHOW_LOCK,CARDRIDGE_REMOVED(sic),PLAY_FIRE,PLAY_SHTDN(_RDY),PLAY_LOCK_SHTDN,PLAY_LOCK_FROM_SLEEP, andFIND_MY_PLAY(Apple Find My).
(The OEM overlaps some strings in flash — e.g. SHIPPING is the tail of
SET_SHIPPING — which the behaviour-equivalent C reproduces as plain literals.)
The UART debug console (login-gated by login_handler) dispatches typed tokens
through a 49-entry command table at flash 0x0804F5C4 — 12-byte
{name, help, handler} entries, extracted verbatim from the OEM image and mapped
in docs/console.md. A fan-out batch then sourced 11 of the richest handlers
into console.c (12 analysed, all leaf; show named+documented). 19/49
handlers are now decoded.
- Raw Modbus injection (the diagnostic bus surface).
bwritereg/breadreghit the battery/BMS (Modbus slave0xAA);swritedata/sreadreghit the shifter (slave0x20). Function codes:0x03read-holding-registers,0x06write-single-register,0x10write-multiple-registers. They build a raw frame and push it viamodbus_bat_submit(0x08039DDC) /modbus_shift_submit(0x080378A0). (swritedatacarries an OEM latent overflow — no length bound — preserved faithfully.) - Modem (tracking).
gsminfodumps the cached u-blox identity — manufacturer/ model/fw/imei/imsi/iccid/csq from the modem-info block at*(ctx+0x3E8)(16-byte text fields) plus the BLE MAC atctx+0x390.gsmstartrestarts the SMS-info state machine (zeroes its step byte0x200000E5). Three commands open sub-shells by UART redirect:bledebug→UART7 (CC2642),gsmdebug→UART2 (modem AT),bmsdebug→Modbus. - Config / modes.
distance(ctx+0x31C, tenths-km),wheelsize(ctx+0x10B, 24/28-inch + dual-bank persist),speed(ctx+0x3C4/+0x3C6 override),shipping(bike state → 7), andfactory-shipping(0x08041FF8) — the full powerdown: drop the GPIO power rails, deinit subsystems, BLE notify (cmd0x112) + Modbus cmd0x14draining each TX queue, thenshipping_powerdown_deinit(6)(no return). show(0x08042714, named) is theParametersdump — ~62 log lines that map most of thectx_sub(session_ctx) struct: I/O pins, Error Flags atctx+0x3BC/+0x3B8, Ibat+0x3B2, volumes+0x105/6/7, region+0x109, wheel+0x10B, light mode+0x10C, alarm/lock state+0x310..+0x318, distance+0x31C, modem info+0x3E8, BLE MAC+0x390, powerbank S/N+ver+0x3D5.., SOC+0x3D4.- Corrections the table forced: the volume handlers are
vollow/volmid/volhigh→ ctx+0x105/+0x106/+0x107(theapp_state.hstruct + the two handler names were off by one; fixed, and the missingvolhighsourced). There is nosoccommand — the consolegearcommand runsconsole_soc_set(writes+0x3D4, prints "Set SOC %d"): a firmware relabel, now documented. - Verifier catch: several agents modelled the app-context base
0x20009368as a pointer (one deref too many); the correct model is the existingg_app_state(struct base,ctx_subpointer at+0x2DC) — all handlers integrated that way.
A second console fan-out sourced 13 more handlers into console.c (ver is
named+documented). The first lean-prompt run failed entirely (all 14 agents
explored but never emitted the structured result — the recurring StructuredOutput
flakiness); a tightened, tool-call-first re-run succeeded.
- Subsystem telemetry refresh + scheduled dump pattern.
battery,shifterstatus, andmotorstatuseach clear a shadow region inctx_sub, issue Modbus reads, and arm a periodic scheduler task whose callback prints the populated telemetry:battery→ BMS shadowctx+0x3F2(300 B) + two reads (regs 0x00–0x30, 0x47–0x56)- 1 s
console_battery_dump;shifterstatus→ 24 V rail (GPIOB p14) + HW-version poll (ctx+0x520);motorstatus→ clearsctx+0x364..0x376, 500 ms poll + four motor Modbus requests (cmd 0x0C/0x0A/0x0B/0x0D).
- 1 s
- ADC rails (
adc) —hw_version_lookup+supply_voltage_read(Vbat),adc_read_vgsm,adc_read_5vsw; all scale raw·3300/4096 mV off the ADC sample buffer at0x20000914.stcreads the battery STC fuel-gauge (stc_read) → "%d mV, %d mA %d%dC %d.%d%%". - Log control —
logprndumps the circular buffer (log_buffer_dump) bracketed by suppressing live logging viastate_flag_set(0/1)(so0x20000083is the live-log-suppress flag);logclrwipes it only with the confirmation key6. - Modes/system —
batware(start batteryware FW update: state 0x19 +batteryware_update_armsets*(0x20008A00+6)=4),factory(settings_factory_reset),reboot(deferredreboot_restart_taskafter 600 ticks),sound(channel_notify_with_status(idx)), and the last Modbus injectorsbwritedata(battery FC 0x10) /swritereg(shifter FC 0x06). ver(named) is the version/identity dump (app fw0x08020004, boot0x08007FDC, model stringctx+0x64A, serial/MACctx+0x390).
The final console fan-out sourced the last 16 handlers, completing the entire 49-command surface. Highlights:
- Sub-shell openers.
bledebug/gsmdebugset a console UART-redirect selector byte (ctx+0x34C/ctx+0x34D) so the main loop bridges the console to UART7 (the CC2642 BLE chip) / UART2 (the u-blox modem);shiftwareuses the global selector at0x2000019C(=2).bmsdebug/shiftdebugare not redirects — they toggle Modbus debug-print flags (ctx+0x34F/ctx+0x34E). helpwalks the 49-entry dispatch table at0x0804F5C4printing each{name, help}(confirming the table layout).logoutclears the sessionlogged_inbyte (ctx+0x2D9).- Hardware resets.
bleresetpulses GPIOE PE5;batresetpulses GPIOB PB5 and schedules a 5 s release callback (which frees its own scheduler slot).stcresetresets the battery gas-gauge. setoadrequests OAD (firmware-update) bike state0x19;setgearwrites a gear-encoder position to the MT shifter (ctx+0x520==0x201) via a Modbus write-multiple. Plus the toggles/timersloop,logapp(log→APP, persists the state record),powerchange,shiftresetcounter.- Verifier catch:
batreset's callbackFUN_080307A8(0x20000112)isscheduler_release(freeing its one-shot slot), not "battery bring-up".
- Scheduled callbacks decoded. The status commands install periodic scheduler
tasks; these are now documented (field maps in their Ghidra plate comments):
console_battery_dump(0x0804175C, ~50-field BMS telemetry, ctx+0x3F2..0x49E, with the −0xAAB Kelvin→Celsius temp offset),motor_get_timer_cb(0x08040DE0, motor telemetry ctx+0x364..0x388),shifterstatus_dump_v200/_v201(0x080410C4/0x08040EEC, the HW-0x200/0x201 shifter dumps incl. the per-gear position loop), andreboot_restart_task(0x08038A68, the multi-state shutdown/power-cycle SM — clears the warm-boot marker, bike state 0x16, BLE notify 0x11D, gas-gauge + factory reset, GPIO power sequencing). The trivialshiftdebug_pump_task(watchdog-kick self-rearm) is sourced intoconsole.c. console.cmade human-readable. Replaced the raw magic in the batch-8/9/10 handlers with named constants: thectx_subbyte viewCTXB,CTX_*field-offset macros (distance/wheelsize/speed/redirect/debug/loop/…),MB_SLAVE_BAT/MB_SLAVE_SHIFT+MB_FUNC_*Modbus codes,GPIO*_BASE, and named scheduler-slot / redirect-selector globals (78 offset renames + the bases).- Consistency fix surfaced by the refactor: those handlers had hardcoded the
OEM app-context address
0x20009368, while the early handlers use the linker-placedg_app_state(a normalextern, not pinned). Routing every handler throughCTXB == g_app_state.ctx_subunifies them on the same object — a behaviour-equivalence correction (verified: only those handlers' literal pools changed,0x20009368→&g_app_state; the early handlers are untouched).
| Address | Size | Name | Source file | Notes |
|---|---|---|---|---|
0x080232E0 |
14 | systick_tick |
src/systick.c |
g_systick_counter += g_systick_step; counter at SRAM 0x20009704, step at SRAM 0x20000014 (shared .data offset with mainboot) |
0x080232F8 |
6 | systick_now |
src/systick.c |
return g_systick_counter (ms since boot); byte-identical to OEM |
0x08023304 |
34 | systick_delay |
src/systick.c |
busy-wait ticks (+g_systick_step) periods; 0xFFFFFFFF = forever |
0x080424A4 |
236 | volume_low_set |
src/console.c |
vollow cmd: show/set ctx_sub->volume_low (+0x105); range [0,64], drives amp, config-persist |
0x080423B8 |
236 | volume_medium_set |
src/console.c |
volmid cmd: writes ctx_sub->volume_medium (+0x106) |
0x080422CC |
236 | volume_high_set |
src/console.c |
volhigh cmd: writes ctx_sub->volume_high (+0x107) |
0x08041360 |
~70 | console_cmd_distance |
src/console.c |
distance cmd: set ctx+0x31C (tenths-km), echo "Set %u.%u Km" |
0x08042120 |
~140 | console_cmd_wheelsize |
src/console.c |
wheelsize cmd: ctx+0x10B (24→0/28→1) + dual-bank config persist |
0x0804131C |
~60 | console_cmd_speed |
src/console.c |
speed cmd: write override to ctx+0x3C4/+0x3C6 |
0x080415D0 |
18 | console_cmd_shipping |
src/console.c |
shipping cmd: request bike state → 7 (created as a function this pass) |
0x08041FF8 |
~250 | console_cmd_factory_shipping |
src/console.c |
factory-shipping: GPIO rails off, BLE 0x112 + Modbus 0x14, powerdown (no return) |
0x08040D14 |
~170 | console_cmd_gsminfo |
src/console.c |
gsminfo cmd: dump u-blox identity (*(ctx+0x3E8)) + BLE MAC (ctx+0x390) |
0x08041D38 |
16 | console_cmd_gsmstart |
src/console.c |
gsmstart cmd: "Start GSM" + zero SMS-info step 0x200000E5 |
0x08041C84 |
~120 | console_cmd_bwritereg |
src/console.c |
bwritereg: Modbus bat (0xAA) write-single-reg (func 0x06) via modbus_bat_submit |
0x08041B30 |
~110 | console_cmd_breadreg |
src/console.c |
breadreg: Modbus bat (0xAA) read-holding-reg (func 0x03) |
0x0804168C |
~190 | console_cmd_swritedata |
src/console.c |
swritedata: Modbus shift (0x20) write-multiple (func 0x10) via modbus_shift_submit |
0x080414A4 |
~130 | console_cmd_sreadreg |
src/console.c |
sreadreg: Modbus shift (0x20) read-holding-reg (func 0x03) |
0x08042F28 |
~110 | console_cmd_battery |
src/console.c |
battery: wipe BMS shadow (ctx+0x3F2), 2 BMS reads, arm 1 s telemetry dump |
0x08042F74 |
~140 | console_cmd_shifterstatus |
src/console.c |
shifterstatus: 24V rail (GPIOB p14), clear ctx+0x51E, HW-ver status poll |
0x08042E54 |
~180 | console_cmd_motorstatus |
src/console.c |
motorstatus: 500ms poll, clear ctx+0x364.., 4 motor Modbus reqs (0x0C/0A/0B/0D) |
0x08043028 |
~120 | console_cmd_adc |
src/console.c |
adc: HW version + Vbat/Vgsm/5Vsw rail mV (raw·3300/4096) |
0x08041614 |
~110 | console_cmd_stc |
src/console.c |
stc: battery STC fuel-gauge V/I/temp/% |
0x08041E50 |
~30 | console_cmd_batware |
src/console.c |
batware: start batteryware FW update (state 0x19 + arm) |
0x08041F88 |
~30 | console_cmd_logprn |
src/console.c |
logprn: dump circular log buffer, suppress live logging during |
0x08041F34 |
~70 | console_cmd_logclr |
src/console.c |
logclr: clear log buffer (requires key 6) |
0x08041E70 |
~30 | console_cmd_factory |
src/console.c |
factory: settings_factory_reset(cfg, 1) |
0x08041DA4 |
18 | console_cmd_reboot |
src/console.c |
reboot: schedule deferred restart task (+600 ticks) |
0x08041D10 |
~30 | console_cmd_sound |
src/console.c |
sound: channel_notify_with_status(idx) |
0x08041BB4 |
~190 | console_cmd_bwritedata |
src/console.c |
bwritedata: Modbus bat (0xAA) write-multiple (func 0x10) |
0x08041528 |
~130 | console_cmd_swritereg |
src/console.c |
swritereg: Modbus shift (0x20) write-single (func 0x06) |
0x08040AA0 |
~40 | console_cmd_help |
src/console.c |
help: walk the 49-entry dispatch table, print name + help |
0x08040A4C |
~8 | console_cmd_logout |
src/console.c |
logout: clear session logged_in byte (ctx+0x2D9) |
0x08041FB8 |
~30 | console_cmd_blereset |
src/console.c |
blereset: pulse BLE reset (GPIOE PE5) high 10ms→low |
0x08040C6C |
~30 | console_cmd_bledebug |
src/console.c |
bledebug: UART7 sub-shell — set redirect ctx+0x34C |
0x08040C28 |
~60 | console_cmd_loop |
src/console.c |
loop: print super-loop timing (ctx+0x358/35C/360), reset accum |
0x08041E94 |
~120 | console_cmd_logapp |
src/console.c |
logapp: log→APP flag (ctx+0x313) + persist state record to EEPROM |
0x080412BC |
~50 | console_cmd_powerchange |
src/console.c |
powerchange: set/report "ride change" flag (ctx+0x3CB) |
0x08041DD8 |
~60 | console_cmd_batreset |
src/console.c |
batreset: pulse BMS reset (GPIOB PB5), 5s release callback |
0x08041DBC |
~30 | console_cmd_shiftware |
src/console.c |
shiftware: start shifter FW update (redirect sel 0x2000019C=2) |
0x08041D50 |
~60 | console_cmd_shiftdebug |
src/console.c |
shiftdebug: toggle Modbus-shifter debug (ctx+0x34E) + pump task |
0x08040CB4 |
~22 | console_cmd_shiftresetcounter |
src/console.c |
shiftresetcounter: zero ctx+0x338 |
0x08040C90 |
~30 | console_cmd_gsmdebug |
src/console.c |
gsmdebug: UART2 (modem) sub-shell — set redirect ctx+0x34D |
0x08040CD8 |
~38 | console_cmd_bmsdebug |
src/console.c |
bmsdebug: toggle Modbus-BMS debug (ctx+0x34F) |
0x080415EC |
~28 | console_cmd_stcreset |
src/console.c |
stcreset: reset battery gas-gauge (gas_gauge_reset) |
0x080415B4 |
~30 | console_cmd_setoad |
src/console.c |
setoad: request OAD update mode (bike state 0x19) |
0x080413B4 |
~190 | console_cmd_setgear |
src/console.c |
setgear: write MT gear position via Modbus shift (func 0x10) |
0x08042590 |
28 | console_start_motor_update |
src/console.c |
logs "Start motor update.." + FUN_080313E4(4) |
0x080425AC |
72 | console_soc_set |
src/console.c |
parses int arg, writes ctx_sub->set_soc (+0x3D4), prints "Set SOC %d", announces via FUN_0802F1C0(2) |
0x080425F4 |
166 | login_handler |
src/console.c |
ES3 debug-console login callback; matches input against g_app_state.ctx_sub->user_password then hard-coded fallback at 0x080547EC; 5-strike → 5 s scheduler-driven lockout |
0x08040A5C |
66 | console_next_token |
src/console.c |
line tokenizer; delimiters space/./:, terminator \0; advances *pp to next token, returns 1 if one exists |
0x080421CC |
220 | console_region_set |
src/console.c |
region command: set region/speed mode ctx[0x109] (0=EU/1=US/2=JP/3=OFFROAD), gated on ctx[0x144]<3; re-applies config via FUN_08031728 |
0x0803F5A4 |
240 | slip_rx_packet |
src/ssp.c |
SLIP de-framer + CRC-16 for the BLE/SSP byte stream; returns 0=frame ready, 2=error, 1=receiving |
0x0803F8FC |
440 | ble_ssp_dispatch |
src/ssp.c |
pull one SSP frame, dispatch by type (0x05 cmd / 0x06 prepare / 0x07 data→ble_cmd_dispatch); ack |
0x0803CF30 |
98 | flash_erase |
src/flash.c |
erase shadow-flash sectors over [addr,addr+len) via flash_erase_sector |
0x0803CF94 |
64 | flash_write |
src/flash.c |
IRQ-off word-program loop (flash_program op 2) into shadow flash; OEM IRQ-on-error quirk preserved |
0x0803C2A8 |
32 | crc16_modbus_update |
src/crc.c |
per-byte CRC-16 step, reflected poly 0xA001 (byte-faithful) |
0x0803C2C8 |
18 | crc16 |
src/crc.c |
CRC-16 over (buf,len,init), loops crc16_modbus_update |
0x08027A04 |
29 | flash_program_word |
src/flash.c |
STM32F4 FLASH PG: set PSIZE=x32 + PG, store triggers word program |
0x0803CF1C |
14 | flash_unlock_and_clear_status |
src/flash.c |
KEY1/KEY2 unlock + clear FLASH_SR (the eraser prep) |
0x080318AE |
62 | ringbuf_get_byte |
src/util.c |
generic byte-FIFO dequeue; behind ssp_rx_byte + ~15 others |
0x0803F9CC |
172 | ssp_ble_enqueue_tx_packet |
src/ssp.c |
BLE/SSP TX queue enqueue (128×12-byte descriptors, rolling seq); ret slot/0xFD/0xFF |
0x080391B8 |
122 | amp_volume_brownout_apply |
src/audio.c |
supply-clamped amp volume apply (PD5/PD13 GPIO + bus TX cmd 0x96); resolved console.c stub |
0x08029B88 |
18 | maybe_set_state_if_unlocked |
src/app.c |
set FSM state byte (SRAM 0x2000002D) unless it's 6/7 (locked) |
0x08029BA0 |
10 | maybe_get_bike_state |
src/app.c |
read the FSM state byte (SRAM 0x2000002D); getter twin of the above |
0x0803B738 |
16 | maybe_set_pending_request |
src/app.c |
latch a u32 request value + pending flag (SRAM 0x20008230) |
0x0802A2F0 |
18 | channel_notify_with_status |
src/app.c |
resolve a channel's status (0..3) and forward to the notify emitter |
0x0803F498 |
~70 | ble_command |
src/ssp.c |
type-0x05 ACK: release the queued TX packet with matching seq id + free its heap payload |
0x0803F6A4 |
8 | ble_data_packet |
src/ssp.c |
type-0x07 thunk → ble_cmd_dispatch |
0x0803F6AC |
8 | ble_prepare_packet |
src/ssp.c |
type-0x06 thunk → ble_read_request_dispatch |
0x0803F4F0 |
~90 | slip_send_frame |
src/ssp.c |
SLIP TX: CRC-16 + 0xC0 framing/escaping; the TX twin of slip_rx_packet |
0x0803DBC8 |
~60 | log_print_timestamp_prefix |
src/log.c |
emit "DD/HH:MM:SS " via g_log_func, bracketed by a flag save/restore |
0x08032D6C |
45 | supply_voltage_read |
src/sensor.c |
raw ADC → fixed-point scale → 10-sample moving average |
0x08032AB0 |
62 | moving_avg10_push |
src/sensor.c |
10-sample circular moving average; shares the ADC ctx struct (0x20000914: cursor +0, samples +4); /10 via 0xCCCCCCCD |
0x080364F0 |
46 | uart_send_byte |
src/uart.c |
serial TX primitive: mask TX IRQ (DSB/ISB) → ringbuf_push_byte → re-enable; implicitly returns ring push status (ABI quirk) |
0x0802A2B0 |
62 | channel_resolve_status |
src/app.c |
resolve channel id vs 3 priority bitmasks (*0x20000944+0xF4/F8/FC); ret 1..3 by priority or 0 |
0x08036B8C |
~8 | state_flag_get |
src/app.c |
read state/mode flag byte at SRAM 0x20000083 |
0x08036B80 |
~8 | state_flag_set |
src/app.c |
write state/mode flag byte at SRAM 0x20000083 (bracketed by log_print_timestamp_prefix) |
0x0802A064 |
~430 | channel_notify_emit |
src/app.c |
sound/clocking scheduler sub-path (codes 5/1B/1C/1D) + amp volume apply + "SOUND_S%c vol %d" + 2-byte BLE notify (SSP cmd 0xC8) |
0x08031874 |
62 | ringbuf_push_byte |
src/util.c |
generic byte-FIFO enqueue; push twin of ringbuf_get_byte (advance head, wrap at cap, bump count) |
0x08036528 |
34 | ssp_rx_byte |
src/ssp.c |
atomic bus-RX-ring pop (RX IRQ masked via *(*0x20009864)+0xC); implicitly returns ring-get status (ABI quirk) |
0x0803F470 |
~40 | ssp_ble_seq_id_in_use |
src/ssp.c |
scan all 128 TX-queue slots' seq byte (+3) for seq; ret 1/0 |
0x0803CE14 |
~90 | flash_addr_to_sector |
src/flash.c |
F413 flash address → erase-sector 0..15 (unsigned-underflow range tests; 16K×4 / 64K / 128K…) |
0x0802968C |
34 | log_buffer_crc_check |
src/log.c |
CRC-16 the 8-byte log-buffer header at SRAM 0x20037000; reset + "Log cleared…" on mismatch (was config_crc_check) |
0x0802320E |
~40 | crc32_hw_feed |
src/crc.c |
feed N words into the STM32 CRC peripheral via the HAL handle @0x20009D90; return accumulated CRC |
0x0802F8A0 |
~50 | bikecomm_request_build |
src/net.c |
snprintf request embedding bikecomm.vanmoof.com; ret 0 ok / 3 error-or-truncated |
0x0802F940 |
~60 | ublox_request_build |
src/net.c |
snprintf request embedding ublox1.vanmoof.com (×2) + token PBNjh0V46Eev8CcfS4LPJg; ret 0/3 |
0x0803CFD8 |
144 | pack_validate |
src/flash.c |
OTA PACK validator: magic 0xAA55AA55, len < 0x40000, HW CRC-32 (header crc/len masked, then body, continuous) vs pack[2]; 0/1/2 |
0x080314E8 |
~430 | gpio_init |
src/gpio.c |
board GPIO bring-up: RCC AHB1 clock enables (E/H/C/A/B/D), initial levels, per-pin HAL_GPIO_Init. The mainware pin map |
0x0803E258 |
~110 | eeprom_write_region |
src/eeprom.c |
AT24C I2C EEPROM page-split (<=8B) write via HAL_I2C_Mem_Write (dev 0xA0); 5 ms + watchdog per page |
0x0802957C |
~70 | log_buffer_reset |
src/log.c |
reset SRAM log buffer: cursors→payload start, revalidate header CRC, zero 100 KB payload, console "Log cleared" |
0x0802963C |
~80 | log_emit_string |
src/log.c |
append a string into the SRAM log ring (oldest-line eviction on overflow); ret length or 0xFFFFFFFF if >= 0x100 |
0x0802F0F8 |
~8 | aux_mode_byte_get |
src/app.c |
read the mode/sub-mode byte at SRAM 0x20000068 |
0x0802E7F4 |
~8 | set_mode_state_byte |
src/app.c |
write the mode/sub-mode byte at SRAM 0x20000068 (the channel_notify_emit clocking sub-mode) |
0x0802F104 |
~90 | enter_mode3_arm_show_timer |
src/app.c |
enter display mode 3: latch prior mode to display field, lazily alloc+arm the "ssp_show_tmr" 4000-tick task |
0x0803C8F4 |
~110 | clock_pulse_gpioa8_until_pc9 |
src/app.c |
I2C3 bus recovery: deinit I2C3, pulse SCL (PA8) until SDA (PC9) high (max 200×), re-init I2C3; ret pulse count ("Clocking %d") |
0x080295D8 |
34 | log_putc |
src/log.c |
write a byte at the log write cursor (header+4), advance + wrap at 0x18C00 end, set overflow flag |
0x080295B4 |
~30 | log_getc |
src/log.c |
read a byte at the log read cursor (header+0), advance + wrap |
0x08029604 |
~50 | log_drain_line |
src/log.c |
pop one line (oldest-first) from the log ring; copy to out if non-NULL; re-stamp header CRC |
0x08029564 |
~30 | log_buffer_header_crc_update |
src/log.c |
CRC-16 over the 8-byte log header → header+8 |
0x0803B780 |
~40 | reset_dual_buffers_and_flags |
src/app.c |
zero the two 0x96-byte buffers in g_request_ctx (0x20008230+0x01/+0x99), set +0x132, clear +0x130/1 |
0x080313D8 |
~8 | update_mode_get |
src/app.c |
read the update-mode/link-ready byte at SRAM 0x20000076 (2 = idle/ready) |
0x0803C8E4 |
~12 | i2c3_handle_deinit |
src/i2c.c |
thunk → HAL_I2C_DeInit on the I2C3 handle (0x20009B04); used in bus recovery |
0x0803C660 |
~80 | i2c3_handle_init |
src/i2c.c |
populate + HAL_I2C_Init the I2C3 handle (Instance I2C3 0x40005C00, 100 kHz, 7-bit) |
0x08029B70 |
2 | scheduler_set_timer_name |
src/scheduler.c |
bx lr stub (debug timer-name register), paired with scheduler_start |
0x080314D8 |
12 | watchdog_kick |
src/watchdog.c |
refresh the WWDG: thunk → wdg_reg_write_from_desc (writes 0x7F to WWDG_CR) |
0x08026DB4 |
~16 | wdg_reg_write_from_desc |
src/watchdog.c |
descriptor-driven 32-bit store: *desc[0] = desc[3] (desc@0x20009728: WWDG_CR ← 0x7F) |
0x08032DF0 |
~520 | alarm_state_name |
src/states.c |
alarm/bike state→name table (62 cases, tbh jump table); all strings read from the OEM image — incl. OAD/PIN/lock/FIND_MY_PLAY |
0x08031444 |
~40 | watchdog_init |
src/watchdog.c |
build the WWDG descriptor @0x20009728 {WWDG_CR,0x180,0x7F,0x7F,0} + wwdg_hw_init; Error_Handler on failure |
0x08026D8A |
~30 | wwdg_hw_init |
src/watchdog.c |
HAL_WWDG_Init: clock-enable + `WWDG_CR=T |
0x08031474 |
~20 | wwdg_clk_enable |
src/watchdog.c |
`RCC_APB1ENR |
0x0803DDCC |
~12 | Error_Handler |
src/panic.c |
CubeF4 fatal handler: g_log_func("Error_Handler\r\n") + spin; shared by WWDG/I2C init failures |
0x080306D8 |
96 | scheduler_tick |
src/scheduler.c |
SysTick dispatch over 48 armed slots; decrement counter (floored at 0), fire callback once when it lands on 1 |
0x0803073C |
100 | scheduler_alloc |
src/scheduler.c |
first-free slot from allocated bitmap (+0x00); zero counter+cb, set bit; on full table → muco_assert_fail (no return) |
0x080307A8 |
84 | scheduler_release |
src/scheduler.c |
clear both armed(+0x08)+allocated(+0x00) bits, zero counter+cb, write *slot_ref=0xFA; ret 1/0 |
0x08030800 |
50 | scheduler_start |
src/scheduler.c |
store counter=ticks, cb; set armed bit; re-arm just resets the counter |
0x08030838 |
26 | scheduler_slot_is_idle |
src/scheduler.c |
slot<48 && counters[slot]==0 |
0x080306C0 |
20 | scheduler_init |
src/scheduler.c |
clear both slot bitmaps (all 48 free) at startup; returns 1 |
0x0803C974 |
12 | NMI_Handler |
src/exceptions.c |
log "NMI_Handler\r\n", return |
0x0803C988 |
18 | HardFault_Handler |
src/exceptions.c |
naked; pick MSP/PSP via tst lr,#4, tail-call fault_dump (assembles byte-for-byte) |
0x0803C99C |
12 | MemManage_Handler |
src/exceptions.c |
log, spin (b .) |
0x0803C9B0 |
12 | BusFault_Handler |
src/exceptions.c |
log, spin |
0x0803C9C4 |
12 | UsageFault_Handler |
src/exceptions.c |
log, spin |
0x0803C9D8 |
12 | SVC_Handler |
src/exceptions.c |
log, return |
0x0803C9EC |
12 | DebugMon_Handler |
src/exceptions.c |
log, return |
0x0803CA00 |
12 | PendSV_Handler |
src/exceptions.c |
log, return |
0x0803CA14 |
12 | SysTick_Handler |
src/exceptions.c |
scheduler_tick() then systick_tick() |
0x0803CB6C |
166 | fault_dump |
src/exceptions.c |
print stacked R0-R3/R12/LR/PC/xPSR + SCB MMFAR/BFAR/CFSR/HFSR/DFSR/AFSR, spin |
0x0803DAC4 |
16 | muco_assert_fail |
src/panic.c |
g_log_func("FATAL error File [%s] line [%d]\r\n", file, line), spin; no-return |
0x080313E4 |
16 | update_mode_request |
src/app.c |
set subsystem update-mode byte (SRAM 0x20000076) iff currently idle (==2) |
0x0802F1C0 |
28 | announce_mark |
src/app.c |
set broadcast dirty-flag for channel 0/1 (SRAM 0x2000028D/0x2000028E); other channels no-op |
0x0802311C |
18 | bcd_to_bin |
src/util.c |
packed-BCD byte → binary (bcd&0xF)+(bcd>>4)*10 |
0x08022F2E |
22 | bin_to_bcd |
src/util.c |
binary byte → packed BCD (count-tens loop) |
0x08043AA4 |
72 | SystemInit |
src/system_stm32f413.c |
FPU enable + RCC reset + VTOR=0x08000000; OEM op-sequence match |
| Address | Size | Name | Source file | Notes |
|---|---|---|---|---|
0x08043E54 |
72 | Reset_Handler |
src/startup_stm32f413.S |
set SP, copy .data, zero .bss, paint [_ebss,_estack) with 0x0000000E, then SystemInit→__libc_init_array→main. Asm (startup); co-located with the vector table + envelope + Default_Handler. |
| Address | Size | Name | Source |
|---|---|---|---|
0x08020980 |
198 | __floatsidf |
libgcc soft-float signed-int → double (0x432 exponent bias, 0x100000 mantissa MSB). mainware does double math in software (the FPU is single-precision only). |
0x08020DEC |
6 | __getreent |
newlib reentrancy-pointer getter — returns _impure_ptr (*0x2000011C), the _REENT that strtol & friends use. |
0x08020EC0 |
348 | memcpy |
newlib optimised memcpy — unaligned head fix-up, 16-word (64-byte) unrolled main loop, 4-word / word / half / byte tails; returns dst. Supplies the ctx_sub[0x104..0x1C4] snapshot in volume_set_common. |
0x08021428 |
730 | strcmp |
newlib/glibc optimised C strcmp (byte fast-path + 4/8-byte aligned word compares using uadd8/sel). Returns *s1 - *s2 of first differing byte. Will pick up from vendored newlib once that's wired in. |
0x08021EAC |
18 | strtol |
newlib reentrant trampoline: loads _REENT from *0x2000011C and tail-calls _strtol_r at 0x08021D5C. Standard long strtol(const char *s, char **endptr, int base). |
0x080212B0 |
136 | snprintf |
newlib snprintf core (sets up the _REENT string-FILE, calls the vfprintf engine 0x0802210C, NUL-terminates; EOVERFLOW on negative size). The cloud request builders format through it. |
0x08026AC6 |
12 | HAL_GPIO_WritePin |
CubeF4 HAL inline: writes pin_mask to GPIOx->BSRR if state non-zero, otherwise pin_mask << 16 (atomic bit-set / bit-reset). |
0x08020DF8 |
100 | __libc_init_array |
newlib C-runtime init: runs __preinit_array, calls _init, runs __init_array. Called second by Reset_Handler. |
0x08043EBC |
4 | _init |
empty newlib/crti _init stub (bx lr), called by __libc_init_array. |
0x08020E40 |
16 | malloc |
newlib malloc(size) — loads _impure_ptr (*0x2000011C) and tail-calls _malloc_r. ssp_ble_enqueue_tx_packet allocates TX payload copies through it. |
0x08020E50 |
~20 | free |
newlib free(ptr) — loads _impure_ptr and tail-calls _free_r. ble_command frees the TX payload through it. |
0x08027BE0 |
116 | HAL_FLASH_Program |
CubeF4 HAL: width-tagged program (TypeProgram 0/1/2/3 = byte/half/word/dword), Data is a u64. Reentrancy-guarded + locked. flash_write calls it (word). |
0x08027B14 |
? | HAL_FLASH_Unlock |
CubeF4 HAL: KEY1/KEY2 unlock of FLASH_CR (idempotent — checks LOCK first). Distinct from the muco flash_unlock_and_clear_status. |
0x08027B80 |
? | FLASH_WaitForLastOperation |
CubeF4 HAL: poll FLASH_SR BSY with a tick timeout + clear handle ErrorCode. Was mislabelled lock_acquire — flash_write waits-for-op before programming. |
0x08023234 |
? | HAL_CRC_Accumulate |
CubeF4 HAL: accumulate a word buffer through the CRC peripheral (sibling of the bespoke crc32_hw_feed). |
0x080267D0 |
? | HAL_GPIO_Init |
CubeF4 HAL: configure a port's pins (MODER/OTYPER/OSPEEDR/PUPDR/AFR) from a GPIO_InitTypeDef. Driven by gpio_init + clock_pulse_gpioa8_until_pc9. |
0x08026AB8 |
? | HAL_GPIO_ReadPin |
CubeF4 HAL: 1 if any masked GPIOx->IDR bit set, else 0. |
0x080235B4 |
? | HAL_FLASHEx_Erase |
CubeF4 HAL: sector/mass erase (FLASH_EraseInitTypeDef + &SectorError). The flash_erase sector callee. |
0x08024D2C |
? | HAL_I2C_Mem_Write |
CubeF4 HAL: blocking I2C memory write (dev/mem-addr/size/timeout). Used by eeprom_write_region. |
0x08021740 |
? | strlen |
newlib strlen (word-at-a-time NUL scan). Used by log_emit_string. |
0x08024570 |
? | HAL_I2C_Init |
CubeF4 HAL: configure an I2C peripheral from the handle's Init fields. Used by i2c3_handle_init. |
0x0802472C |
? | HAL_I2C_DeInit |
CubeF4 HAL: disable + reset an I2C peripheral handle. Used by i2c3_handle_deinit. |
| Address | Size | Name | Why named |
|---|---|---|---|
0x0803DEA8 |
613 | main |
application entry / super-loop, called last by Reset_Handler. Structurally decoded (see "Application structure" above); body sourcing waits on its ~60 callees. Currently a weak spin-placeholder in startup_stm32f413.S so the tree links. |
0x08031900 |
~2.6K | subsystem_update_sm |
whole-bike OTA firmware-update state machine (see "Subsystem firmware update" above). Deep — dozens of callees; mapped, not sourced. |
0x0802AAF8 |
~14K | status_process |
the per-loop bike-status/event processor (main calls it each iteration); emits Diag + Error Flags. Too large for the decompiler — see "Diagnostics / error flags". |
0x08033970 |
~2.5K | ble_cmd_dispatch |
the BLE app-command surface — ~40-case switch on the 16-bit command id (lock/region/power/LED/backup-code/…). Full map in docs/ble-commands.md. Mapped, not sourced. |
0x0803E2CC |
65 | save_state_record_to_eeprom |
CRC + write the 56-byte state record ctx[0x310..0x348] to I2C AT24C EEPROM (0xA0), double-stored at 0/0x40. Named only — stack-aliasing ABI. |
0x08029CA0 |
830 | testmode_command_dispatch |
"SH Set testmode" cmd (sel 0–12); sel 1 = version/serial/HW telemetry over BLE svc 0x554A–0x5550; sel 2–C arm a 64-bit action mask. |
0x08034D20 |
~3.9K | ble_read_request_dispatch |
GATT/SSP characteristic-READ dispatcher (~45 cases 0x5503–0x55C1); read twin of ble_cmd_dispatch. |
0x08031728 |
82 | config_persist_dual_bank |
persist a config snapshot to two internal-flash banks (0x08008000 / 0x0800C000) via a flash erase+write+CRC worker; OR'd status. Stack-aliasing ABI — named only. |
0x080380A4 |
? | rtc_fill_time_fields |
fill a buffer with current RTC time fields via STM32 HAL GetTime/GetDate ([0]=hr [1]=min [2]=sec [0x16]=day). |
0x080316D0 |
82 | flash_config_bank_write |
single-bank config writer under config_persist_dual_bank: erase → CRC-in-record → write 0xD0 → CRC-verify. Stack-aliasing ABI — named only. |
0x0803FC94 |
~1473 | mainware_boot_init_sequence |
top-level startup orchestrator (version print, GPIO/flash bring-up, per-subsystem init + fault-retry/recovery, state-record validation, dual-bank config persist). Hint "flash routine" corrected. |
0x0803CC6C |
? | sms_info_tracking_state_machine |
modem SMS info-tracking scheduler (4-state, anti-theft tracking path). Hint "SSP service" corrected. |
0x0803F338 |
? | modbus_bat_service_step |
per-loop battery (BMS) Modbus link service step (pump SM, retry/flush on 0x80000 region tag). Distinct from BLE/SSP. |
0x0803A1C4 |
? | maybe_enqueue_tx_message |
generic enqueue into a 16×24-byte TX table at 0x20007E14, gated on link-connected. Hint "GATT send" corrected. |
0x0803DDE0 |
? | boot_init_cold |
main's cold-boot path — taken when SRAM 0x20000000 != 0x55AA55CF; precedes the full init sequence. |
0x0803DADC |
? | boot_init_warm |
main's warm-boot path — taken when the 0x55AA55CF retained-RAM marker is present (skips cold init). Contains muco_assert_fail calls. |
Deferred (the remaining capstone):
main(0x0803DEA8) +boot_init_cold/boot_init_warm— the super-loop capstone. Already structurally mapped in "Application structure" below; full C sourcing is deep-document (depends on the last ~dozen un-named loop callees) and may want address-pinning to be meaningful. Left named.
(The batch-7 WWDG-builder + I2C error-trap completers are now done —
watchdog_init/wwdg_hw_init/wwdg_clk_enable → watchdog.c, the shared
Error_Handler → panic.c; watchdog.c + i2c.c are fully self-contained.)
Keystone — DONE. startup_stm32f413.S (envelope + 128-entry vector table +
Reset_Handler + Default_Handler), system_stm32f413.c (SystemInit), and
log.c (the g_log_func global) now root the graph via ENTRY(Reset_Handler)
KEEP(.isr_vector). The build links to a real, non-empty image (maketext ≈ 2 KB skeleton: envelope, vectors, the boot path, and the SysTick closure).make compareis live again. Two pending callees that the OEMReset_Handlerinvokes are weak placeholders until sourced:main(decoded, spins) and__libc_init_array(newlib, returns).
Byte-match status (read before trusting make compare). The 512-B
envelope reproduces byte-perfectly except 7 bytes — the CRC32 (+0x08) and
total length (+0x0C), both post-build patches. SystemInit compiles to the
OEM's exact operation sequence (even the two-bic split of 0xFEF6FFFF).
But the overall image is not byte-identical: our decoded functions land at
linker-assigned addresses, not their OEM addresses, so the vector table values
and every .text body differ. A meaningful full-image diff (where the
differing-byte count drops as decode coverage grows) needs address pinning
— placing each decoded function at its OEM address in the linker script and
filling the gaps — i.e. a matching-decomp setup. That's a strategic build change
worth deciding deliberately; flagged here, not done. The FPU is enabled
(SystemInit sets CPACR), so a pinned/complete build will want
-mfloat-abi=hard -mfpu=fpv4-sp-d16 once FP-using functions are decoded.
Console-handler callees still opaque (the remaining extern FUN_* stubs in
console.c):
| Address | Size | Notes |
|---|---|---|
0x08031728 |
? | Audio-engine apply (4-word arg). Called from volume_*_set after every volume change with the four words at ctx_sub->audio_engine_cfg[0..3]. |
0x080391B8 |
? | Volume validator. Called with a pointer to the just-parsed volume byte; non-zero return triggers a " ERR set volume" log. |
0x080313E4 |
? | Subsystem-mode request. Called as (4) from console_start_motor_update. |
0x0802F1C0 |
? | Broadcast/announce. Called as (2) from console_soc_set after writing the SOC override. |
Full list in ghidra/exports/mainware_program.json (refresh via
ghidra/scripts/DumpMainwareProgram.java).
-
Hard-coded debug-console password in flash.
login_handler(0x080425F4) accepts a 40-character fallback"vEVjGF!paYs M2EBV8SoDT8*T0eB&#T6xevaoxCaO"stored verbatim in rodata at0x080547EC. Path-tracing confirms the fallback is accepted regardless of whether the user has set their own service password —strcmp(input, "")never returns 0 for a non-empty input (and empty inputs are filtered out earlier), so the user-password compare always falls through to the hard-coded one when the user-side slot is empty. The hand-written sanity guarduser_password[0] != '\0'is dead-code under standardstrcmpsemantics. Worth checking whethermainware_1.08.02.binandmainware_1.09.*.binstill ship the same constant. The 5-strike / 5-second lockout via the Muco scheduler is the only brute-force mitigation, and any input typed during cooldown re-arms the lockout to a fresh 5 s.Persistence across versions. Verified by
strings | grep: the exact 40-character constant appears once in each ofmainware_1.07.06.bin(Nov 2021),mainware_1.08.02.bin(May 2022),mainware_1.09.01.bin(May 2023), andmainware_1.09.03.bin(Jun 2023). The backdoor was shipped unchanged for ~19 months across the entire visible release history. -
Hard-coded u-blox cloud auth token in flash.
ublox_request_build(0x0802F940) formats the modem HTTP request with the token"PBNjh0V46Eev8CcfS4LPJg"stored verbatim at0x080508D4, alongside the endpoint hostublox1.vanmoof.com(0x080508C0);bikecomm_request_build(0x0802F8A0) targetsbikecomm.vanmoof.com(0x08050890). The token is a static credential the modem presents to the VanMoof backend — shared across the fleet (same firmware image), not device-derived. Worth checking whether it rotates across the 1.08/1.09 releases.
- Exact mainware flash slot —
0x08040000is the working hypothesis from VTOR alignment; confirm frommainboot's "Jump to App" code path. - VTOR set by mainboot before jump — must equal the slot base
0x08040000(or the per-slot equivalent) for the table at file offset0x200to dispatch correctly. - FPU usage — does any function emit
vpush/vpop(would require switching to-mfloat-abi=hard -mfpu=fpv4-sp-d16)? - Modem command flow — is the uBlox SARA driver a clean state machine
or a soup of inline
printf+expectcalls? - BLE-side protocol — the bleware on the CC2642 talks to mainware over Modbus (same bus the shifter and motor use)? Or a separate UART / SPI link?
- What is at file offset
0x010..0x028exactly — the ASCII build date+time looks like literal__DATE__+__TIME__placed at a known location by the linker script. Confirm by inspecting the early.textfor a reference to0x08040010.